Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
2348d95
progress
arr00 Jan 8, 2026
c642cda
progress
arr00 Jan 12, 2026
4dde8eb
up
arr00 Jan 14, 2026
c87e19f
add mocks
arr00 Jan 20, 2026
33c573f
mv file
arr00 Jan 20, 2026
7f91019
up and quit
arr00 Jan 22, 2026
80a1228
Merge branch 'master' into feat/batcher
arr00 Jan 22, 2026
2c670f8
Add docs
arr00 Jan 22, 2026
1790102
Fix import order
arr00 Jan 22, 2026
6e0684e
fix mock
arr00 Jan 22, 2026
f6cd2ec
Remove .only
arr00 Jan 22, 2026
dce9c87
fix pragma
arr00 Jan 22, 2026
84315d6
fix test
arr00 Jan 22, 2026
fc11a75
fix codespell
arr00 Jan 22, 2026
5d01683
add tests
arr00 Jan 22, 2026
59c1128
fix lint
arr00 Jan 22, 2026
cd3735e
rename `exit` to `claim` and `quit` to `cancel`
arr00 Jan 23, 2026
97d4739
add testing for quit
arr00 Jan 23, 2026
c355860
modify exchange rate type to uint64
arr00 Jan 23, 2026
d0311e6
add docs
arr00 Jan 23, 2026
3d6522d
nit
arr00 Jan 23, 2026
f16b837
Merge branch 'master' into feat/batcher
arr00 Jan 24, 2026
ab44326
Merge branch 'master' into feat/batcher
arr00 Jan 24, 2026
fe407cd
reorder ops in join
arr00 Jan 26, 2026
94d5b5d
update mantissa and ensure that exchange rate is valid
arr00 Jan 26, 2026
46be773
update tests
arr00 Jan 26, 2026
8244ba5
add warning
arr00 Jan 27, 2026
9e5544c
add events
arr00 Jan 28, 2026
58af428
rename cancel to quit
arr00 Jan 29, 2026
3e4c4a1
return amount from functions
arr00 Jan 29, 2026
5a5f09d
don't assume transfer succeeded on batcher
arr00 Jan 29, 2026
d5e0fb4
Batcher: store cleartext batch total (#301)
arr00 Jan 30, 2026
db2d1c0
review feedback
arr00 Feb 2, 2026
617afde
Update contracts/mocks/utils/BatcherConfidentialSwapMock.sol
arr00 Feb 2, 2026
c11bd1a
review feedback
arr00 Feb 2, 2026
3dcf817
correct exchange rate precision renaming
arr00 Feb 3, 2026
3dff1dc
use getters when possible
arr00 Feb 4, 2026
4a12cde
exchange rate of 0 is invalid
arr00 Feb 5, 2026
a5b1d0d
state machine
arr00 Feb 7, 2026
6994796
add cancelled state
arr00 Feb 8, 2026
2aea578
allow quitting if state is cancelled
arr00 Feb 9, 2026
bce33a4
move join to internal function. Add callback support. return unproces…
arr00 Feb 9, 2026
15cfd92
spelling nit
arr00 Feb 10, 2026
b0d5d06
review feedback
arr00 Feb 11, 2026
b24d9b1
bug fixes
arr00 Feb 11, 2026
7a8d425
test callback usage
arr00 Feb 11, 2026
1ab0541
`_executeRoute` swaps between ERC20s only (no wrapping)
arr00 Feb 13, 2026
ac81367
Update BatcherConfidential.sol
Amxx Feb 18, 2026
94a5fef
review feedback
arr00 Feb 20, 2026
302af5a
inline set exchange rate
arr00 Feb 20, 2026
aa57944
stop storing total deposits cleartext
arr00 Feb 20, 2026
4cbb77b
remove join function
arr00 Feb 20, 2026
2a174c9
use abi.encodeWithSignature
Amxx Feb 20, 2026
b374eb4
add test showcasing the cancel + quit bug
Amxx Feb 20, 2026
161cb81
Fix bug in the cancel workflow (#309)
Amxx Feb 22, 2026
2ec257e
add tests
arr00 Feb 22, 2026
49fd716
add test
arr00 Feb 22, 2026
b209463
add changeset
arr00 Feb 22, 2026
b9aca9f
update docs
arr00 Feb 23, 2026
9ceb3f0
update docs
arr00 Feb 23, 2026
683010e
add reentrancy protection to `dispatchBatchCallback`
arr00 Mar 3, 2026
eb23ca3
cancel if batch amount is 0
arr00 Mar 3, 2026
11793fe
lint fix
arr00 Mar 3, 2026
5d6c8f5
fix tests
arr00 Mar 3, 2026
f5a6ecd
L-03 add docs
arr00 Mar 4, 2026
417c7dd
update docs
arr00 Mar 4, 2026
5d1ed4a
L-01 warning
arr00 Mar 4, 2026
2783026
create internal function `_getAndIncreaseBatchId`
arr00 Mar 4, 2026
f58da88
Move batcher to finance
arr00 Mar 4, 2026
85b57f5
update files and docs for move
arr00 Mar 4, 2026
10b624d
move test file
arr00 Mar 5, 2026
9be290f
change logic to check transfer success on claim
arr00 Mar 5, 2026
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/modern-snakes-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-confidential-contracts': minor
---

`BatcherConfidential`: A batching primitive that enables routing between two {ERC7984ERC20Wrapper} contracts via a non-confidential route.
401 changes: 401 additions & 0 deletions contracts/finance/BatcherConfidential.sol

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions contracts/finance/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This directory includes primitives for on-chain confidential financial systems:

- {VestingWalletConfidential}: Handles the vesting of confidential tokens for a given beneficiary. Custody of multiple tokens can be given to this contract, which will release the token to the beneficiary following a given, customizable, vesting schedule.
- {VestingWalletCliffConfidential}: Variant of {VestingWalletConfidential} which adds a cliff period to the vesting schedule.
- {BatcherConfidential}: A batching primitive that enables routing between two {ERC7984ERC20Wrapper} contracts via a non-confidential route.

For convenience, this directory also includes:

Expand All @@ -18,3 +19,4 @@ For convenience, this directory also includes:
{{VestingWalletConfidential}}
{{VestingWalletCliffConfidential}}
{{VestingWalletConfidentialFactory}}
{{BatcherConfidential}}
81 changes: 81 additions & 0 deletions contracts/mocks/finance/BatcherConfidentialSwapMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {FHE, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {BatcherConfidential} from "./../../finance/BatcherConfidential.sol";
import {ExchangeMock} from "./../finance/ExchangeMock.sol";

abstract contract BatcherConfidentialSwapMock is ZamaEthereumConfig, BatcherConfidential {
ExchangeMock public exchange;
address public admin;
ExecuteOutcome public outcome = ExecuteOutcome.Complete;

constructor(ExchangeMock exchange_, address admin_) {
exchange = exchange_;
admin = admin_;
}

function routeDescription() public pure override returns (string memory) {
return "Exchange fromToken for toToken by swapping through the mock exchange.";
}

function setExecutionOutcome(ExecuteOutcome outcome_) public {
outcome = outcome_;
}

/// @dev Join the current batch with `externalAmount` and `inputProof`.
function join(externalEuint64 externalAmount, bytes calldata inputProof) public virtual returns (euint64) {
euint64 amount = FHE.fromExternal(externalAmount, inputProof);
FHE.allowTransient(amount, address(fromToken()));
euint64 transferred = fromToken().confidentialTransferFrom(msg.sender, address(this), amount);

euint64 joinedAmount = _join(msg.sender, transferred);
euint64 refundAmount = FHE.sub(transferred, joinedAmount);

FHE.allowTransient(refundAmount, address(fromToken()));

fromToken().confidentialTransfer(msg.sender, refundAmount);

return joinedAmount;
}

function join(uint64 amount) public {
euint64 ciphertext = FHE.asEuint64(amount);
FHE.allowTransient(ciphertext, msg.sender);

bytes memory callData = abi.encodeWithSignature(
"join(bytes32,bytes)",
externalEuint64.wrap(euint64.unwrap(ciphertext)),
hex""
);

Address.functionDelegateCall(address(this), callData);
}

function quit(uint256 batchId) public virtual override returns (euint64) {
euint64 amount = super.quit(batchId);
FHE.allow(totalDeposits(batchId), admin);
return amount;
}

function _join(address to, euint64 amount) internal virtual override returns (euint64) {
euint64 joinedAmount = super._join(to, amount);
FHE.allow(totalDeposits(currentBatchId()), admin);
return joinedAmount;
}

function _executeRoute(uint256, uint256 unwrapAmount) internal override returns (ExecuteOutcome) {
if (outcome == ExecuteOutcome.Complete) {
// Approve exchange to spend unwrapped tokens
uint256 rawAmount = unwrapAmount * fromToken().rate();
IERC20(fromToken().underlying()).approve(address(exchange), rawAmount);

// Swap unwrapped tokens via exchange
exchange.swapAToB(rawAmount);
}
return outcome;
}
}
38 changes: 38 additions & 0 deletions contracts/mocks/finance/ExchangeMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";

contract ExchangeMock {
IERC20 public tokenA;
IERC20 public tokenB;
uint256 public exchangeRate;

event ExchangeRateSet(uint256 oldExchangeRate, uint256 newExchangeRate);

constructor(IERC20 tokenA_, IERC20 tokenB_, uint256 initialExchangeRate) {
tokenA = tokenA_;
tokenB = tokenB_;
exchangeRate = initialExchangeRate;
}

function swapAToB(uint256 amount) public returns (uint256) {
uint256 amountOut = (amount * exchangeRate) / 1e18;
require(tokenA.transferFrom(msg.sender, address(this), amount), "Transfer of token A failed");
require(tokenB.transfer(msg.sender, amountOut), "Transfer of token B failed");
return amountOut;
}

function swapBToA(uint256 amount) public returns (uint256) {
uint256 amountOut = (amount * 1e18) / exchangeRate;
require(tokenB.transferFrom(msg.sender, address(this), amount), "Transfer of token B failed");
require(tokenA.transfer(msg.sender, amountOut), "Transfer of token A failed");
return amountOut;
}

function setExchangeRate(uint256 newExchangeRate) public {
emit ExchangeRateSet(exchangeRate, newExchangeRate);

exchangeRate = newExchangeRate;
}
}
24 changes: 21 additions & 3 deletions contracts/mocks/token/ERC7984ERC20WrapperMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,32 @@
pragma solidity ^0.8.27;

import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {euint64} from "@fhevm/solidity/lib/FHE.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {ERC7984ERC20Wrapper, ERC7984} from "../../token/ERC7984/extensions/ERC7984ERC20Wrapper.sol";
import {ERC7984ERC20Wrapper, ERC7984} from "./../../token/ERC7984/extensions/ERC7984ERC20Wrapper.sol";
import {ERC7984Mock} from "./ERC7984Mock.sol";

contract ERC7984ERC20WrapperMock is ERC7984ERC20Wrapper, ZamaEthereumConfig {
contract ERC7984ERC20WrapperMock is ERC7984ERC20Wrapper, ZamaEthereumConfig, ERC7984Mock {
constructor(
IERC20 token,
string memory name,
string memory symbol,
string memory uri
) ERC7984ERC20Wrapper(token) ERC7984(name, symbol, uri) {}
) ERC7984ERC20Wrapper(token) ERC7984Mock(name, symbol, uri) {}

function supportsInterface(bytes4 interfaceId) public view override(ERC7984ERC20Wrapper, ERC7984) returns (bool) {
return super.supportsInterface(interfaceId);
}

function decimals() public view override(ERC7984ERC20Wrapper, ERC7984) returns (uint8) {
return super.decimals();
}

function _update(
address from,
address to,
euint64 amount
) internal virtual override(ERC7984ERC20Wrapper, ERC7984Mock) returns (euint64) {
return super._update(from, to, amount);
}
}
4 changes: 4 additions & 0 deletions contracts/mocks/token/ERC7984Mock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ contract ERC7984Mock is ERC7984, ZamaEthereumConfig {
return _transferAndCall(from, to, FHE.fromExternal(encryptedAmount, inputProof), data);
}

function $_burn(address from, uint64 amount) public returns (euint64 transferred) {
return _burn(from, FHE.asEuint64(amount));
}

function $_burn(
address from,
externalEuint64 encryptedAmount,
Expand Down
Loading