diff --git a/.changeset/fast-humans-run.md b/.changeset/fast-humans-run.md deleted file mode 100644 index 7166ce4f..00000000 --- a/.changeset/fast-humans-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-confidential-contracts': patch ---- - -`ERC7984Restricted`, `ERC7984Rwa`: Rename `isUserAllowed` to `canTransact` diff --git a/.changeset/full-worlds-rescue.md b/.changeset/full-worlds-rescue.md deleted file mode 100644 index bcdd34bf..00000000 --- a/.changeset/full-worlds-rescue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-confidential-contracts': minor ---- - -Migrate `@fhevm/solidity` dependency to `0.11.1` diff --git a/.changeset/good-flies-win.md b/.changeset/good-flies-win.md deleted file mode 100644 index fa7ea7cd..00000000 --- a/.changeset/good-flies-win.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-confidential-contracts': minor ---- - -Migrate `@fhevm/solidity` dependency to `0.10.0` diff --git a/.changeset/happy-hands-find.md b/.changeset/happy-hands-find.md deleted file mode 100644 index 552eb471..00000000 --- a/.changeset/happy-hands-find.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-confidential-contracts': minor ---- - -Upgrade openzeppelin/contracts and openzeppelin/contracts-upgradeable to v5.6.1 diff --git a/.changeset/long-items-appear.md b/.changeset/long-items-appear.md deleted file mode 100644 index 1c07d5ae..00000000 --- a/.changeset/long-items-appear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-confidential-contracts': minor ---- - -`ERC7984ERC20Wrapper`: Support ERC-165 interface detection on `ERC7984ERC20Wrapper`. diff --git a/.changeset/modern-snakes-return.md b/.changeset/modern-snakes-return.md deleted file mode 100644 index f660fd96..00000000 --- a/.changeset/modern-snakes-return.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-confidential-contracts': minor ---- - -`BatcherConfidential`: A batching primitive that enables routing between two {ERC7984ERC20Wrapper} contracts via a non-confidential route. diff --git a/.changeset/nasty-walls-taste.md b/.changeset/nasty-walls-taste.md deleted file mode 100644 index dc20f5da..00000000 --- a/.changeset/nasty-walls-taste.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-confidential-contracts': patch ---- - -`ERC7984ERC20Wrapper`: revert on wrap if there is a chance of total supply overflow. diff --git a/.changeset/pink-areas-double.md b/.changeset/pink-areas-double.md deleted file mode 100644 index d77d9d3e..00000000 --- a/.changeset/pink-areas-double.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-confidential-contracts': minor ---- - -`ERC7984ERC20Wrapper`: return the amount of wrapped token sent on wrap calls. diff --git a/.changeset/shaky-pugs-return.md b/.changeset/shaky-pugs-return.md deleted file mode 100644 index bc65f265..00000000 --- a/.changeset/shaky-pugs-return.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-confidential-contracts': minor ---- - -`HandleAccessManager`: change `_validateHandleAllowance` to return a boolean and validate it. diff --git a/.changeset/sour-dodos-sniff.md b/.changeset/sour-dodos-sniff.md deleted file mode 100644 index c02eebc3..00000000 --- a/.changeset/sour-dodos-sniff.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-confidential-contracts': minor ---- - -`ERC7984ERC20Wrapper`: return unwrapped amount on `unwrap` calls diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index abc933c9..26bd7612 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -6,7 +6,7 @@ runs: steps: - uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 24.x - uses: actions/cache@v4 id: cache with: diff --git a/CHANGELOG.md b/CHANGELOG.md index fe958c12..28f682fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,28 @@ # openzeppelin-confidential-contracts +## 0.4.0 (2026-03-20) + +- Migrate `@fhevm/solidity` dependency to `0.11.1` ([#311](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/311)) +- Upgrade openzeppelin/contracts and openzeppelin/contracts-upgradeable to v5.6.1 ([#314](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/314)) + +### Token + +- `ERC7984ERC20Wrapper`: use a bytes32 unwrap request identifier instead of identifying batches by the euint64 unwrap amount. ([#326](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/326)) +- `ERC7984ERC20Wrapper`: Support ERC-165 interface detection on `ERC7984ERC20Wrapper`. ([#267](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/267)) +- `ERC7984ERC20Wrapper`: return the amount of wrapped token sent on wrap calls. ([#307](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/307)) +- `ERC7984ERC20Wrapper`: return unwrapped amount on `unwrap` calls ([#288](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/288)) +- `ERC7984ERC20Wrapper`: revert on wrap if there is a chance of total supply overflow. ([#268](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/268)) +- `ERC7984Restricted`, `ERC7984Rwa`: Rename `isUserAllowed` to `canTransact` ([#291](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/291)) + +### Finance + +- `BatcherConfidential`: A batching primitive that enables routing between two `ERC7984ERC20Wrapper` contracts via a non-confidential route. ([#293](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/293)) + +### Utils + +- `HandleAccessManager`: change `_validateHandleAllowance` to return a boolean and validate it. ([#303](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/303)) + ## 0.3.1 (2026-01-06) ### Bug fixes diff --git a/contracts/finance/BatcherConfidential.sol b/contracts/finance/BatcherConfidential.sol index 3a919109..2bd13dbc 100644 --- a/contracts/finance/BatcherConfidential.sol +++ b/contracts/finance/BatcherConfidential.sol @@ -1,15 +1,17 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (finance/BatcherConfidential.sol) pragma solidity ^0.8.27; import {FHE, externalEuint64, euint64, ebool, euint128} from "@fhevm/solidity/lib/FHE.sol"; import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import {IERC7984ERC20Wrapper} from "./../interfaces/IERC7984ERC20Wrapper.sol"; import {IERC7984Receiver} from "./../interfaces/IERC7984Receiver.sol"; -import {ERC7984ERC20Wrapper} from "./../token/ERC7984/extensions/ERC7984ERC20Wrapper.sol"; import {FHESafeMath} from "./../utils/FHESafeMath.sol"; /** @@ -23,6 +25,11 @@ import {FHESafeMath} from "./../utils/FHESafeMath.sol"; * * Developers must also implement the virtual function {routeDescription} to provide a human readable description of the batch's route. * + * Claim outputs are rounded down. This may result in small deposits being rounded down to 0 if the exchange rate is less than 1:1. + * {toToken} dust from rounding down will accumulate in the batcher over time. + * + * NOTE: The batcher does not support {ERC7984ERC20Wrapper} contracts prior to v0.4.0. + * * NOTE: The batcher could be used to maintain confidentiality of deposits--by default there are no confidentiality guarantees. * If desired, developers should consider restricting certain functions to increase confidentiality. * @@ -48,14 +55,14 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei struct Batch { euint64 totalDeposits; - euint64 unwrapAmount; + bytes32 unwrapRequestId; uint64 exchangeRate; bool canceled; mapping(address => euint64) deposits; } - ERC7984ERC20Wrapper private immutable _fromToken; - ERC7984ERC20Wrapper private immutable _toToken; + IERC7984ERC20Wrapper private immutable _fromToken; + IERC7984ERC20Wrapper private immutable _toToken; mapping(uint256 => Batch) private _batches; uint256 private _currentBatchId; @@ -80,6 +87,9 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei /// @dev The `batchId` does not exist. Batch IDs start at 1 and must be less than or equal to {currentBatchId}. error BatchNonexistent(uint256 batchId); + /// @dev The `account` has a zero deposits in batch `batchId`. + error ZeroDeposits(uint256 batchId, address account); + /** * @dev The batch `batchId` is in the state `current`, which is invalid for the operation. * The `expectedStates` is a bitmap encoding the expected/allowed states for the operation. @@ -97,7 +107,19 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei /// @dev The caller is not authorized to call this function. error Unauthorized(); - constructor(ERC7984ERC20Wrapper fromToken_, ERC7984ERC20Wrapper toToken_) { + /// @dev The given `token` does not support `IERC7984ERC20Wrapper` via `ERC165`. + error InvalidWrapperToken(address token); + + constructor(IERC7984ERC20Wrapper fromToken_, IERC7984ERC20Wrapper toToken_) { + require( + ERC165Checker.supportsInterface(address(fromToken_), type(IERC7984ERC20Wrapper).interfaceId), + InvalidWrapperToken(address(fromToken_)) + ); + require( + ERC165Checker.supportsInterface(address(toToken_), type(IERC7984ERC20Wrapper).interfaceId), + InvalidWrapperToken(address(toToken_)) + ); + _fromToken = fromToken_; _toToken = toToken_; _currentBatchId = 1; @@ -106,31 +128,13 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei SafeERC20.forceApprove(IERC20(toToken().underlying()), address(toToken()), type(uint256).max); } - /// @dev Claim the `toToken` corresponding to deposit in batch with id `batchId`. - function claim(uint256 batchId) public virtual nonReentrant returns (euint64) { - _validateStateBitmap(batchId, _encodeStateBitmap(BatchState.Finalized)); - - euint64 deposit = deposits(batchId, msg.sender); - - // Overflow is not possible on mul since `type(uint64).max ** 2 < type(uint128).max`. - // Given that the output of the entire batch must fit in uint64, individual user outputs must also fit. - euint64 amountToSend = FHE.asEuint64( - FHE.div(FHE.mul(FHE.asEuint128(deposit), exchangeRate(batchId)), uint128(10) ** exchangeRateDecimals()) - ); - FHE.allowTransient(amountToSend, address(toToken())); - - euint64 amountTransferred = toToken().confidentialTransfer(msg.sender, amountToSend); - - ebool transferSuccess = FHE.ne(amountTransferred, FHE.asEuint64(0)); - euint64 newDeposit = FHE.select(transferSuccess, FHE.asEuint64(0), deposit); - - FHE.allowThis(newDeposit); - FHE.allow(newDeposit, msg.sender); - _batches[batchId].deposits[msg.sender] = newDeposit; - - emit Claimed(batchId, msg.sender, amountTransferred); - - return amountTransferred; + /** + * @dev Claim the `toToken` corresponding to `account`'s deposit in batch with id `batchId`. + * + * NOTE: This function is not gated and can be called by anyone. Claims could be frontrun. + */ + function claim(uint256 batchId, address account) public virtual nonReentrant returns (euint64) { + return _claim(batchId, account); } /** @@ -139,11 +143,16 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei * * NOTE: Developers should consider adding additional restrictions to this function * if maintaining confidentiality of deposits is critical to the application. + * + * WARNING: {dispatchBatch} may fail if an incompatible version of {ERC7984ERC20Wrapper} is used. + * This function must be unrestricted in cases where batch dispatching fails. */ function quit(uint256 batchId) public virtual nonReentrant returns (euint64) { _validateStateBitmap(batchId, _encodeStateBitmap(BatchState.Pending) | _encodeStateBitmap(BatchState.Canceled)); euint64 deposit = deposits(batchId, msg.sender); + require(FHE.isInitialized(deposit), ZeroDeposits(batchId, msg.sender)); + euint64 totalDeposits_ = totalDeposits(batchId); FHE.allowTransient(deposit, address(fromToken())); @@ -174,9 +183,12 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei euint64 amountToUnwrap = totalDeposits(batchId); FHE.allowTransient(amountToUnwrap, address(fromToken())); - _batches[batchId].unwrapAmount = _calculateUnwrapAmount(amountToUnwrap); - - fromToken().unwrap(address(this), address(this), amountToUnwrap); + _batches[batchId].unwrapRequestId = fromToken().unwrap( + address(this), + address(this), + externalEuint64.wrap(euint64.unwrap(amountToUnwrap)), + "" + ); emit BatchDispatched(batchId); } @@ -193,14 +205,14 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei ) public virtual nonReentrant { _validateStateBitmap(batchId, _encodeStateBitmap(BatchState.Dispatched)); - euint64 unwrapAmount_ = unwrapAmount(batchId); + bytes32 unwrapRequestId_ = unwrapRequestId(batchId); // finalize unwrap call will fail if already called by this contract or by anyone else - try ERC7984ERC20Wrapper(fromToken()).finalizeUnwrap(unwrapAmount_, unwrapAmountCleartext, decryptionProof) { + try IERC7984ERC20Wrapper(fromToken()).finalizeUnwrap(unwrapRequestId_, unwrapAmountCleartext, decryptionProof) { // No need to validate input since `finalizeUnwrap` request succeeded } catch { // Must validate input since `finalizeUnwrap` request failed bytes32[] memory handles = new bytes32[](1); - handles[0] = euint64.unwrap(unwrapAmount_); + handles[0] = euint64.unwrap(fromToken().unwrapAmount(unwrapRequestId_)); FHE.checkSignatures(handles, abi.encode(unwrapAmountCleartext), decryptionProof); } @@ -242,7 +254,14 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei } } - /// @inheritdoc IERC7984Receiver + /** + * @dev See {IERC7984Receiver-onConfidentialTransferReceived}. + * + * Deposit {fromToken} into the current batch. + * + * NOTE: See {_claim} to understand how the {toToken} amount is calculated. Claim amounts are rounded down. Small + * deposits may be rounded down to 0 if the exchange rate is less than 1:1. + */ function onConfidentialTransferReceived( address, address from, @@ -256,12 +275,12 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei } /// @dev Batcher from token. Users deposit this token in exchange for {toToken}. - function fromToken() public view virtual returns (ERC7984ERC20Wrapper) { + function fromToken() public view virtual returns (IERC7984ERC20Wrapper) { return _fromToken; } /// @dev Batcher to token. Users receive this token in exchange for their {fromToken} deposits. - function toToken() public view virtual returns (ERC7984ERC20Wrapper) { + function toToken() public view virtual returns (IERC7984ERC20Wrapper) { return _toToken; } @@ -270,9 +289,9 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei return _currentBatchId; } - /// @dev The amount of {fromToken} unwrapped during {dispatchBatch} for batch with id `batchId`. - function unwrapAmount(uint256 batchId) public view virtual returns (euint64) { - return _batches[batchId].unwrapAmount; + /// @dev The unwrap request id for a batch with id `batchId`. + function unwrapRequestId(uint256 batchId) public view virtual returns (bytes32) { + return _batches[batchId].unwrapRequestId; } /// @dev The total deposits made in batch with id `batchId`. @@ -306,7 +325,7 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei if (exchangeRate(batchId) != 0) { return BatchState.Finalized; } - if (euint64.unwrap(unwrapAmount(batchId)) != 0) { + if (unwrapRequestId(batchId) != 0) { return BatchState.Dispatched; } if (batchId == currentBatchId()) { @@ -316,6 +335,37 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei revert BatchNonexistent(batchId); } + /** + * @dev Claims `toToken` for `account`'s deposit in batch with id `batchId`. Tokens are always + * sent to `account`, enabling third-party relayers to claim on behalf of depositors. + */ + function _claim(uint256 batchId, address account) internal virtual returns (euint64) { + _validateStateBitmap(batchId, _encodeStateBitmap(BatchState.Finalized)); + + euint64 deposit = deposits(batchId, account); + require(FHE.isInitialized(deposit), ZeroDeposits(batchId, account)); + + // Overflow is not possible on mul since `type(uint64).max ** 2 < type(uint128).max`. + // Given that the output of the entire batch must fit in uint64, individual user outputs must also fit. + euint64 amountToSend = FHE.asEuint64( + FHE.div(FHE.mul(FHE.asEuint128(deposit), exchangeRate(batchId)), uint128(10) ** exchangeRateDecimals()) + ); + FHE.allowTransient(amountToSend, address(toToken())); + + euint64 amountTransferred = toToken().confidentialTransfer(account, amountToSend); + + ebool transferSuccess = FHE.ne(amountTransferred, FHE.asEuint64(0)); + euint64 newDeposit = FHE.select(transferSuccess, FHE.asEuint64(0), deposit); + + FHE.allowThis(newDeposit); + FHE.allow(newDeposit, account); + _batches[batchId].deposits[account] = newDeposit; + + emit Claimed(batchId, account, amountTransferred); + + return amountTransferred; + } + /** * @dev Joins a batch with amount `amount` on behalf of `to`. Does not do any transfers in. * Returns the amount joined with. @@ -352,8 +402,16 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei * If a multi-step route is necessary, intermediate steps should return `ExecuteOutcome.Partial`. Intermediate steps *must* not * result in underlying {toToken} being transferred into the batcher. * - * WARNING: This function must eventually return `ExecuteOutcome.Complete` or `ExecuteOutcome.Cancel`. Failure to do so results + * [WARNING] + * ==== + * This function must eventually return `ExecuteOutcome.Complete` or `ExecuteOutcome.Cancel`. Failure to do so results * in user deposits being locked indefinitely. + * + * Additionally, the following must hold: + * + * - `swappedAmount >= ceil(unwrapAmountCleartext / 10 ** exchangeRateDecimals()) * toToken().rate()` (the exchange rate must not be 0) + * - `swappedAmount \<= type(uint64).max * toToken().rate()` (the wrapped amount of {toToken} must fit in `uint64`) + * ==== */ function _executeRoute(uint256 batchId, uint256 amount) internal virtual returns (ExecuteOutcome); @@ -371,15 +429,6 @@ abstract contract BatcherConfidential is ReentrancyGuardTransient, IERC7984Recei return currentState; } - /// @dev Mirror calculations done on the token to attain the same cipher-text for unwrap tracking. - function _calculateUnwrapAmount(euint64 requestedUnwrapAmount) internal virtual returns (euint64) { - euint64 balance = fromToken().confidentialBalanceOf(address(this)); - - (ebool success, ) = FHESafeMath.tryDecrease(balance, requestedUnwrapAmount); - - return FHE.select(success, requestedUnwrapAmount, FHE.asEuint64(0)); - } - /// @dev Gets the current batch id and increments it. function _getAndIncreaseBatchId() internal virtual returns (uint256) { return _currentBatchId++; diff --git a/contracts/finance/VestingWalletConfidential.sol b/contracts/finance/VestingWalletConfidential.sol index cbc6b228..65838f8f 100644 --- a/contracts/finance/VestingWalletConfidential.sol +++ b/contracts/finance/VestingWalletConfidential.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.3.0) (finance/VestingWalletConfidential.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (finance/VestingWalletConfidential.sol) pragma solidity ^0.8.24; import {FHE, ebool, euint64, euint128} from "@fhevm/solidity/lib/FHE.sol"; diff --git a/contracts/governance/utils/VotesConfidential.sol b/contracts/governance/utils/VotesConfidential.sol index da6da73c..f7f99567 100644 --- a/contracts/governance/utils/VotesConfidential.sol +++ b/contracts/governance/utils/VotesConfidential.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.2.0) (governance/utils/VotesConfidential.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (governance/utils/VotesConfidential.sol) pragma solidity ^0.8.26; diff --git a/contracts/interfaces/IERC7984ERC20Wrapper.sol b/contracts/interfaces/IERC7984ERC20Wrapper.sol index 73c02b51..539f33b9 100644 --- a/contracts/interfaces/IERC7984ERC20Wrapper.sol +++ b/contracts/interfaces/IERC7984ERC20Wrapper.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (interfaces/IERC7984ERC20Wrapper.sol) pragma solidity ^0.8.24; @@ -7,6 +8,17 @@ import {IERC7984} from "./IERC7984.sol"; /// @dev Interface for ERC7984ERC20Wrapper contract. interface IERC7984ERC20Wrapper is IERC7984 { + /// @dev Emitted when an unwrap request is made for a given `receiver`, `unwrapRequestId`, and `amount`. + event UnwrapRequested(address indexed receiver, bytes32 indexed unwrapRequestId, euint64 amount); + + /// @dev Emitted when an unwrap request is finalized for a given `receiver`, `unwrapRequestId`, `encryptedAmount`, and `cleartextAmount`. + event UnwrapFinalized( + address indexed receiver, + bytes32 indexed unwrapRequestId, + euint64 encryptedAmount, + uint64 cleartextAmount + ); + /** * @dev Wraps `amount` of the underlying token into a confidential token and sends it to `to`. * @@ -18,17 +30,33 @@ interface IERC7984ERC20Wrapper is IERC7984 { * @dev Unwraps tokens from `from` and sends the underlying tokens to `to`. The caller must be `from` * or be an approved operator for `from`. * - * Returns amount unwrapped. + * Returns the unwrap request id. * - * NOTE: The caller *must* already be approved by ACL for the given `amount`. + * NOTE: The returned unwrap request id must never be zero. */ function unwrap( address from, address to, externalEuint64 encryptedAmount, bytes calldata inputProof - ) external returns (euint64); + ) external returns (bytes32); /// @dev Returns the address of the underlying ERC-20 token that is being wrapped. function underlying() external view returns (address); + + /// @dev Finalizes an unwrap request identified by `unwrapRequestId` with the given `unwrapAmountCleartext` and `decryptionProof`. + function finalizeUnwrap( + bytes32 unwrapRequestId, + uint64 unwrapAmountCleartext, + bytes calldata decryptionProof + ) external; + + /** + * @dev Returns the rate at which the underlying token is converted to the wrapped token. + * For example, if the `rate` is 1000, then 1000 units of the underlying token equal 1 unit of the wrapped token. + */ + function rate() external view returns (uint256); + + /// @dev Returns the amount of wrapper tokens that were unwrapped for a given `unwrapRequestId`. + function unwrapAmount(bytes32 unwrapRequestId) external view returns (euint64); } diff --git a/contracts/interfaces/IERC7984Receiver.sol b/contracts/interfaces/IERC7984Receiver.sol index a4d5d24d..a5f4fc4c 100644 --- a/contracts/interfaces/IERC7984Receiver.sol +++ b/contracts/interfaces/IERC7984Receiver.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.3.0) (interfaces/IERC7984Receiver.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (interfaces/IERC7984Receiver.sol) pragma solidity ^0.8.24; import {ebool, euint64} from "@fhevm/solidity/lib/FHE.sol"; diff --git a/contracts/interfaces/IERC7984Rwa.sol b/contracts/interfaces/IERC7984Rwa.sol index ec34ddfc..53a6f617 100644 --- a/contracts/interfaces/IERC7984Rwa.sol +++ b/contracts/interfaces/IERC7984Rwa.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.3.0) (interfaces/IERC7984Rwa.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (interfaces/IERC7984Rwa.sol) pragma solidity ^0.8.24; import {externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol"; diff --git a/contracts/package.json b/contracts/package.json index ac1edeee..8e850545 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@openzeppelin/confidential-contracts", "description": "Smart Contract library for use with confidential coprocessors", - "version": "0.3.1", + "version": "0.4.0", "files": [ "**/*.sol", "/build/contracts/*.json", diff --git a/contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol b/contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol index ce907966..2e268571 100644 --- a/contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol +++ b/contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.3.1) (token/ERC7984/extensions/ERC7984ERC20Wrapper.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (token/ERC7984/extensions/ERC7984ERC20Wrapper.sol) pragma solidity ^0.8.27; @@ -27,12 +27,9 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC7984ERC20Wrapper, IERC1363 uint8 private immutable _decimals; uint256 private immutable _rate; - mapping(euint64 unwrapAmount => address recipient) private _unwrapRequests; + mapping(bytes32 unwrapRequestId => address recipient) private _unwrapRequests; - event UnwrapRequested(address indexed receiver, euint64 amount); - event UnwrapFinalized(address indexed receiver, euint64 encryptedAmount, uint64 cleartextAmount); - - error InvalidUnwrapRequest(euint64 amount); + error InvalidUnwrapRequest(bytes32 unwrapRequestId); error ERC7984TotalSupplyOverflow(); constructor(IERC20 underlying_) { @@ -94,7 +91,7 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC7984ERC20Wrapper, IERC1363 } /// @dev Unwrap without passing an input proof. See {unwrap-address-address-bytes32-bytes} for more details. - function unwrap(address from, address to, euint64 amount) public virtual returns (euint64) { + function unwrap(address from, address to, euint64 amount) public virtual returns (bytes32) { require(FHE.isAllowed(amount, msg.sender), ERC7984UnauthorizedUseOfEncryptedAmount(amount, msg.sender)); return _unwrap(from, to, amount); } @@ -109,22 +106,24 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC7984ERC20Wrapper, IERC1363 address to, externalEuint64 encryptedAmount, bytes calldata inputProof - ) public virtual returns (euint64) { + ) public virtual returns (bytes32) { return _unwrap(from, to, FHE.fromExternal(encryptedAmount, inputProof)); } - /// @dev Fills an unwrap request for a given cipher-text `unwrapAmount` with the `cleartextAmount` and `decryptionProof`. + /// @inheritdoc IERC7984ERC20Wrapper function finalizeUnwrap( - euint64 unwrapAmount, + bytes32 unwrapRequestId, uint64 unwrapAmountCleartext, bytes calldata decryptionProof ) public virtual { - address to = unwrapRequester(unwrapAmount); - require(to != address(0), InvalidUnwrapRequest(unwrapAmount)); - delete _unwrapRequests[unwrapAmount]; + address to = unwrapRequester(unwrapRequestId); + require(to != address(0), InvalidUnwrapRequest(unwrapRequestId)); + + euint64 unwrapAmount_ = unwrapAmount(unwrapRequestId); + delete _unwrapRequests[unwrapRequestId]; bytes32[] memory handles = new bytes32[](1); - handles[0] = euint64.unwrap(unwrapAmount); + handles[0] = euint64.unwrap(unwrapAmount_); bytes memory cleartexts = abi.encode(unwrapAmountCleartext); @@ -132,7 +131,7 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC7984ERC20Wrapper, IERC1363 SafeERC20.safeTransfer(IERC20(underlying()), to, unwrapAmountCleartext * rate()); - emit UnwrapFinalized(to, unwrapAmount, unwrapAmountCleartext); + emit UnwrapFinalized(to, unwrapRequestId, unwrapAmount_, unwrapAmountCleartext); } /// @inheritdoc ERC7984 @@ -140,10 +139,7 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC7984ERC20Wrapper, IERC1363 return _decimals; } - /** - * @dev Returns the rate at which the underlying token is converted to the wrapped token. - * For example, if the `rate` is 1000, then 1000 units of the underlying token equal 1 unit of the wrapped token. - */ + /// @inheritdoc IERC7984ERC20Wrapper function rate() public view virtual returns (uint256) { return _rate; } @@ -153,6 +149,11 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC7984ERC20Wrapper, IERC1363 return address(_underlying); } + /// @inheritdoc IERC7984ERC20Wrapper + function unwrapAmount(bytes32 unwrapRequestId) public view virtual returns (euint64) { + return euint64.wrap(unwrapRequestId); + } + /// @inheritdoc IERC165 function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC7984) returns (bool) { return @@ -182,8 +183,8 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC7984ERC20Wrapper, IERC1363 * @dev 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. */ - function unwrapRequester(euint64 unwrapAmount) public view virtual returns (address) { - return _unwrapRequests[unwrapAmount]; + function unwrapRequester(bytes32 unwrapRequestId) public view virtual returns (address) { + return _unwrapRequests[unwrapRequestId]; } /** @@ -207,24 +208,25 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC7984ERC20Wrapper, IERC1363 return super._update(from, to, amount); } - /// @dev Internal logic for handling the creation of unwrap requests. - function _unwrap(address from, address to, euint64 amount) internal virtual returns (euint64) { + /// @dev Internal logic for handling the creation of unwrap requests. Returns the unwrap request id. + function _unwrap(address from, address to, euint64 amount) internal virtual returns (bytes32) { require(to != address(0), ERC7984InvalidReceiver(to)); require(from == msg.sender || isOperator(from, msg.sender), ERC7984UnauthorizedSpender(from, msg.sender)); // try to burn, see how much we actually got - euint64 unwrapAmount = _burn(from, amount); - FHE.makePubliclyDecryptable(unwrapAmount); + euint64 unwrapAmount_ = _burn(from, amount); + FHE.makePubliclyDecryptable(unwrapAmount_); - assert(unwrapRequester(unwrapAmount) == address(0)); + assert(unwrapRequester(euint64.unwrap(unwrapAmount_)) == address(0)); - // WARNING: Storing unwrap requests in a mapping from cipher-text to address assumes that + // WARNING: Directly using the cipher-text as the unwrap request id assumes that // cipher-texts are unique--this holds here but is not always true. Be cautious when assuming // cipher-text uniqueness. - _unwrapRequests[unwrapAmount] = to; + bytes32 unwrapRequestId = euint64.unwrap(unwrapAmount_); + _unwrapRequests[unwrapRequestId] = to; - emit UnwrapRequested(to, unwrapAmount); - return unwrapAmount; + emit UnwrapRequested(to, unwrapRequestId, unwrapAmount_); + return unwrapRequestId; } /** diff --git a/contracts/token/ERC7984/extensions/ERC7984Freezable.sol b/contracts/token/ERC7984/extensions/ERC7984Freezable.sol index a7d38873..90039f9c 100644 --- a/contracts/token/ERC7984/extensions/ERC7984Freezable.sol +++ b/contracts/token/ERC7984/extensions/ERC7984Freezable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.3.0) (token/ERC7984/extensions/ERC7984Freezable.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (token/ERC7984/extensions/ERC7984Freezable.sol) pragma solidity ^0.8.27; diff --git a/contracts/token/ERC7984/extensions/ERC7984ObserverAccess.sol b/contracts/token/ERC7984/extensions/ERC7984ObserverAccess.sol index 088fb8ef..17fa5416 100644 --- a/contracts/token/ERC7984/extensions/ERC7984ObserverAccess.sol +++ b/contracts/token/ERC7984/extensions/ERC7984ObserverAccess.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.3.0) (token/ERC7984/extensions/ERC7984ObserverAccess.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (token/ERC7984/extensions/ERC7984ObserverAccess.sol) pragma solidity ^0.8.27; diff --git a/contracts/token/ERC7984/extensions/ERC7984Restricted.sol b/contracts/token/ERC7984/extensions/ERC7984Restricted.sol index 4d487ba2..17101f61 100644 --- a/contracts/token/ERC7984/extensions/ERC7984Restricted.sol +++ b/contracts/token/ERC7984/extensions/ERC7984Restricted.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.3.0) (token/ERC7984/extensions/ERC7984Restricted.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (token/ERC7984/extensions/ERC7984Restricted.sol) pragma solidity ^0.8.27; diff --git a/contracts/token/ERC7984/extensions/ERC7984Rwa.sol b/contracts/token/ERC7984/extensions/ERC7984Rwa.sol index 84f1b57b..7d1ee279 100644 --- a/contracts/token/ERC7984/extensions/ERC7984Rwa.sol +++ b/contracts/token/ERC7984/extensions/ERC7984Rwa.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.3.0) (token/ERC7984/extensions/ERC7984Rwa.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (token/ERC7984/extensions/ERC7984Rwa.sol) pragma solidity ^0.8.27; diff --git a/contracts/token/ERC7984/extensions/ERC7984Votes.sol b/contracts/token/ERC7984/extensions/ERC7984Votes.sol index 67b5d3d5..b5d29939 100644 --- a/contracts/token/ERC7984/extensions/ERC7984Votes.sol +++ b/contracts/token/ERC7984/extensions/ERC7984Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.3.0) (token/ERC7984/extensions/ERC7984Votes.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (token/ERC7984/extensions/ERC7984Votes.sol) pragma solidity ^0.8.27; import {euint64} from "@fhevm/solidity/lib/FHE.sol"; diff --git a/contracts/utils/FHESafeMath.sol b/contracts/utils/FHESafeMath.sol index 21a92dff..2e9c06c6 100644 --- a/contracts/utils/FHESafeMath.sol +++ b/contracts/utils/FHESafeMath.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.3.0) (utils/FHESafeMath.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (utils/FHESafeMath.sol) pragma solidity ^0.8.24; import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol"; diff --git a/contracts/utils/HandleAccessManager.sol b/contracts/utils/HandleAccessManager.sol index 5bccbdf2..b9639e89 100644 --- a/contracts/utils/HandleAccessManager.sol +++ b/contracts/utils/HandleAccessManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.2.0) (utils/HandleAccessManager.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (utils/HandleAccessManager.sol) pragma solidity ^0.8.26; import {Impl} from "@fhevm/solidity/lib/Impl.sol"; diff --git a/contracts/utils/structs/CheckpointsConfidential.sol b/contracts/utils/structs/CheckpointsConfidential.sol index ffe75575..a93e4c2d 100644 --- a/contracts/utils/structs/CheckpointsConfidential.sol +++ b/contracts/utils/structs/CheckpointsConfidential.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (last updated v0.2.0) (utils/structs/CheckpointsConfidential.sol) +// OpenZeppelin Confidential Contracts (last updated v0.4.0) (utils/structs/CheckpointsConfidential.sol) // This file was procedurally generated from scripts/generate/templates/CheckpointsConfidential.js. pragma solidity ^0.8.24; diff --git a/docs/antora.yml b/docs/antora.yml index 0fffc2aa..86d5b7b8 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,6 +1,6 @@ name: confidential-contracts title: Confidential Contracts -version: '0.3' +version: '0.4' prerelease: false nav: - modules/ROOT/nav.adoc diff --git a/docs/templates/properties.js b/docs/templates/properties.js deleted file mode 120000 index 3816e066..00000000 --- a/docs/templates/properties.js +++ /dev/null @@ -1 +0,0 @@ -../../lib/openzeppelin-contracts/docs/templates/properties.js \ No newline at end of file diff --git a/docs/templates/properties.js b/docs/templates/properties.js new file mode 100644 index 00000000..13c58407 --- /dev/null +++ b/docs/templates/properties.js @@ -0,0 +1,88 @@ +const { isNodeType, findAll } = require('solidity-ast/utils'); +const { slug } = require('./helpers'); + +module.exports.anchor = function anchor({ item, contract }) { + let res = ''; + if (contract) { + res += contract.name + '-'; + } + res += item.name; + if ('parameters' in item) { + const signature = item.parameters.parameters.map(v => v.typeName.typeDescriptions.typeString).join(','); + res += slug('(' + signature + ')'); + } + if (isNodeType('VariableDeclaration', item)) { + res += '-' + slug(item.typeName.typeDescriptions.typeString); + } + return res; +}; + +module.exports.fullname = function fullname({ item }) { + let res = ''; + res += item.name; + if ('parameters' in item) { + const signature = item.parameters.parameters.map(v => v.typeName.typeDescriptions.typeString).join(','); + res += slug('(' + signature + ')'); + } + if (isNodeType('VariableDeclaration', item)) { + res += '-' + slug(item.typeName.typeDescriptions.typeString); + } + if (res.charAt(res.length - 1) === '-') { + return res.slice(0, -1); + } + return res; +}; + +module.exports.inheritance = function ({ item, build }) { + if (!isNodeType('ContractDefinition', item)) { + throw new Error('inheritance modifier used on non-contract'); + } + + return item.linearizedBaseContracts + .map(id => build.deref('ContractDefinition', id)) + .filter((c, i) => c.name !== 'Context' || i === 0); +}; + +module.exports['has-functions'] = function ({ item }) { + return item.inheritance.some(c => c.functions.length > 0); +}; + +module.exports['has-events'] = function ({ item }) { + return item.inheritance.some(c => c.events.length > 0); +}; + +module.exports['has-errors'] = function ({ item }) { + return item.inheritance.some(c => c.errors.length > 0); +}; + +module.exports['internal-variables'] = function ({ item }) { + return item.variables.filter(({ visibility }) => visibility === 'internal'); +}; + +module.exports['has-internal-variables'] = function ({ item }) { + return module.exports['internal-variables']({ item }).length > 0; +}; + +module.exports.functions = function ({ item }) { + return [ + ...[...findAll('FunctionDefinition', item)].filter(f => f.visibility !== 'private'), + ...[...findAll('VariableDeclaration', item)].filter(f => f.visibility === 'public'), + ]; +}; + +module.exports.returns2 = function ({ item }) { + if (isNodeType('VariableDeclaration', item)) { + return [{ type: item.typeName.typeDescriptions.typeString }]; + } else { + return item.returns; + } +}; + +module.exports['inherited-functions'] = function ({ item }) { + const { inheritance } = item; + const baseFunctions = new Set(inheritance.flatMap(c => c.functions.flatMap(f => f.baseFunctions ?? []))); + return inheritance.map((contract, i) => ({ + contract, + functions: contract.functions.filter(f => !baseFunctions.has(f.id) && (f.name !== 'constructor' || i === 0)), + })); +}; diff --git a/package.json b/package.json index 5dcf4a38..5c7c566d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openzeppelin-confidential-contracts", "description": "", - "version": "0.3.1", + "version": "0.4.0", "author": "OpenZeppelin Community ", "license": "MIT", "files": [ diff --git a/test/finance/BatcherConfidential.test.ts b/test/finance/BatcherConfidential.test.ts index 6dceaf22..e6ceaa2e 100644 --- a/test/finance/BatcherConfidential.test.ts +++ b/test/finance/BatcherConfidential.test.ts @@ -105,6 +105,36 @@ describe('BatcherConfidential', function () { }); }); + it('should reject invalid fromToken', async function () { + const confidentialToken = await ethers.deployContract('$ERC7984Mock', ['Mock Token', 'MTK', 'URI']); + + await expect( + ethers.deployContract('$BatcherConfidentialSwapMock', [ + confidentialToken, + this.toToken, + this.exchange, + this.operator, + ]), + ) + .to.be.revertedWithCustomError(this.batcher, 'InvalidWrapperToken') + .withArgs(confidentialToken.target); + }); + + it('should reject invalid toToken', async function () { + const confidentialToken = await ethers.deployContract('$ERC7984Mock', ['Mock Token', 'MTK', 'URI']); + + await expect( + ethers.deployContract('$BatcherConfidentialSwapMock', [ + this.fromToken, + confidentialToken, + this.exchange, + this.operator, + ]), + ) + .to.be.revertedWithCustomError(this.batcher, 'InvalidWrapperToken') + .withArgs(confidentialToken.target); + }); + for (const viaCallback of [true, false]) { describe(`join ${viaCallback ? 'via callback' : 'directly'}`, async function () { const join = async function ( @@ -224,7 +254,7 @@ describe('BatcherConfidential', function () { }); it('should clear deposits', async function () { - await this.batcher.claim(this.batchId); + await this.batcher.claim(this.batchId, this.holder); await expect( fhevm.userDecryptEuint( @@ -244,7 +274,7 @@ describe('BatcherConfidential', function () { this.holder, ); - await this.batcher.claim(this.batchId); + await this.batcher.claim(this.batchId, this.holder); await expect( fhevm.userDecryptEuint( @@ -260,22 +290,26 @@ describe('BatcherConfidential', function () { it('should revert if not finalized', async function () { const currentBatchId = await this.batcher.currentBatchId(); - await expect(this.batcher.claim(currentBatchId)) + await expect(this.batcher.claim(currentBatchId, this.holder)) .to.be.revertedWithCustomError(this.batcher, 'BatchUnexpectedState') .withArgs(currentBatchId, BatchState.Pending, encodeStateBitmap(BatchState.Finalized)); }); + it('should revert if account did not participate in the batch', async function () { + await expect(this.batcher.claim(this.batchId, this.recipient)) + .to.be.revertedWithCustomError(this.batcher, 'ZeroDeposits') + .withArgs(this.batchId, this.recipient.address); + }); + it('should emit event', async function () { - await expect(this.batcher.claim(this.batchId)) + await expect(this.batcher.claim(this.batchId, this.holder)) .to.emit(this.batcher, 'Claimed') .withArgs(this.batchId, this.holder.address, anyValue); }); it('should allow retry claim (idempotent when fully claimed)', async function () { - // First claim should succeed and clear deposits - await this.batcher.claim(this.batchId); + await this.batcher.claim(this.batchId, this.holder); - // Verify deposits are cleared await expect( fhevm.userDecryptEuint( FhevmType.euint64, @@ -285,10 +319,8 @@ describe('BatcherConfidential', function () { ), ).to.eventually.eq(0); - // Second claim should succeed (return 0, no-op since no deposit left) - await expect(this.batcher.claim(this.batchId)).to.emit(this.batcher, 'Claimed'); + await expect(this.batcher.claim(this.batchId, this.holder)).to.emit(this.batcher, 'Claimed'); - // Deposits should still be zero await expect( fhevm.userDecryptEuint( FhevmType.euint64, @@ -303,7 +335,7 @@ describe('BatcherConfidential', function () { // will burn `toToken` from batcher to induce failed transfer await this.toToken['$_burn(address,uint64)'](this.batcher, 100n); - let claimEvent = (await (await this.batcher.claim(this.batchId)).wait()).logs.filter( + let claimEvent = (await (await this.batcher.claim(this.batchId, this.holder)).wait()).logs.filter( (log: any) => log.address === this.batcher.target, )[0]; let claimAmount = claimEvent.args[2]; @@ -314,7 +346,7 @@ describe('BatcherConfidential', function () { await this.toToken['$_mint(address,uint64)'](this.batcher, 100n); - claimEvent = (await (await this.batcher.claim(this.batchId)).wait()).logs.filter( + claimEvent = (await (await this.batcher.claim(this.batchId, this.holder)).wait()).logs.filter( (log: any) => log.address === this.batcher.target, )[0]; claimAmount = claimEvent.args[2]; @@ -323,6 +355,55 @@ describe('BatcherConfidential', function () { fhevm.userDecryptEuint(FhevmType.euint64, claimAmount, this.toToken.target, this.holder), ).to.eventually.eq(1000n); }); + + describe('on behalf of (relayer)', function () { + it('should send tokens to the depositor, not the relayer', async function () { + const relayer = this.accounts[0]; + + const holderBalanceBefore = await fhevm.userDecryptEuint( + FhevmType.euint64, + await this.toToken.confidentialBalanceOf(this.holder), + this.toToken, + this.holder, + ); + + await this.batcher.connect(relayer).claim(this.batchId, this.holder); + + const expectedAmount = BigInt(this.exchangeRate * this.deposit) / exchangeRateMantissa; + + await expect( + fhevm.userDecryptEuint( + FhevmType.euint64, + await this.toToken.confidentialBalanceOf(this.holder), + this.toToken, + this.holder, + ), + ).to.eventually.eq(BigInt(holderBalanceBefore) + expectedAmount); + }); + + it('should clear the depositor deposits', async function () { + const relayer = this.accounts[0]; + + await this.batcher.connect(relayer).claim(this.batchId, this.holder); + + await expect( + fhevm.userDecryptEuint( + FhevmType.euint64, + await this.batcher.deposits(this.batchId, this.holder), + this.batcher, + this.holder, + ), + ).to.eventually.eq(0); + }); + + it('should emit event with the depositor address', async function () { + const relayer = this.accounts[0]; + + await expect(this.batcher.connect(relayer).claim(this.batchId, this.holder)) + .to.emit(this.batcher, 'Claimed') + .withArgs(this.batchId, this.holder.address, anyValue); + }); + }); }); describe('quit', function () { @@ -383,6 +464,12 @@ describe('BatcherConfidential', function () { .withArgs(this.batchId, BatchState.Dispatched, encodeStateBitmap(BatchState.Pending, BatchState.Canceled)); }); + it('should revert if caller did not participate in the batch', async function () { + await expect(this.batcher.connect(this.recipient).quit(this.batchId)) + .to.be.revertedWithCustomError(this.batcher, 'ZeroDeposits') + .withArgs(this.batchId, this.recipient.address); + }); + it('should emit event', async function () { await expect(this.batcher.quit(this.batchId)) .to.emit(this.batcher, 'Quit') @@ -401,7 +488,7 @@ describe('BatcherConfidential', function () { const [, amount] = (await this.fromToken.queryFilter(this.fromToken.filters.UnwrapRequested()))[0].args; const { abiEncodedClearValues, decryptionProof } = await fhevm.publicDecrypt([amount]); - await expect(this.batcher.unwrapAmount(batchId)).to.eventually.eq(amount); + await expect(this.batcher.unwrapRequestId(batchId)).to.eventually.eq(amount); Object.assign(this, { joinAmount, batchId, unwrapAmount: amount, abiEncodedClearValues, decryptionProof }); }); @@ -409,7 +496,7 @@ describe('BatcherConfidential', function () { it('should finalize unwrap', async function () { await expect(this.batcher.dispatchBatchCallback(this.batchId, this.abiEncodedClearValues, this.decryptionProof)) .to.emit(this.fromToken, 'UnwrapFinalized') - .withArgs(this.batcher, this.unwrapAmount, this.abiEncodedClearValues); + .withArgs(this.batcher, this.unwrapAmount, this.unwrapAmount, this.abiEncodedClearValues); }); it('should revert if proof validation fails', async function () { @@ -563,7 +650,7 @@ describe('BatcherConfidential', function () { // dispatch amount is publicly decryptable const { abiEncodedClearValues, decryptionProof } = await batcher - .unwrapAmount(batchId1) + .unwrapRequestId(batchId1) .then(amount => fhevm.publicDecrypt([amount])); expect(abiEncodedClearValues).to.eq(amount1); @@ -621,7 +708,7 @@ describe('BatcherConfidential', function () { // Check unwrap amount await expect( batcher - .unwrapAmount(batchId2) + .unwrapRequestId(batchId2) .then(amount => fhevm.publicDecrypt([amount])) .then(({ abiEncodedClearValues }) => abiEncodedClearValues), ).to.eventually.eq(amount2); diff --git a/test/helpers/interface.ts b/test/helpers/interface.ts index caaccf9a..a59f6e24 100644 --- a/test/helpers/interface.ts +++ b/test/helpers/interface.ts @@ -22,7 +22,14 @@ export const SIGNATURES = { 'setOperator(address,uint48)', 'symbol()', ], - ERC7984ERC20Wrapper: ['underlying()', 'unwrap(address,address,bytes32,bytes)', 'wrap(address,uint256)'], + ERC7984ERC20Wrapper: [ + 'underlying()', + 'unwrap(address,address,bytes32,bytes)', + 'wrap(address,uint256)', + 'finalizeUnwrap(bytes32,uint64,bytes)', + 'rate()', + 'unwrapAmount(bytes32)', + ], ERC7984RWA: [ 'blockUser(address)', 'confidentialAvailable(address)', diff --git a/test/token/ERC7984/extensions/ERC7984ERC20Wrapper.test.ts b/test/token/ERC7984/extensions/ERC7984ERC20Wrapper.test.ts index 7bbe1097..f6b8a159 100644 --- a/test/token/ERC7984/extensions/ERC7984ERC20Wrapper.test.ts +++ b/test/token/ERC7984/extensions/ERC7984ERC20Wrapper.test.ts @@ -390,5 +390,5 @@ async function publicDecryptAndFinalizeUnwrap(wrapper: ERC7984ERC20WrapperMock, const { abiEncodedClearValues, decryptionProof } = await fhevm.publicDecrypt([amount]); await expect(wrapper.connect(caller).finalizeUnwrap(amount, abiEncodedClearValues, decryptionProof)) .to.emit(wrapper, 'UnwrapFinalized') - .withArgs(to, amount, abiEncodedClearValues); + .withArgs(to, amount, amount, abiEncodedClearValues); }