Finance
Smart contract finance utilities and implementations
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 ofVestingWalletConfidentialwhich adds a cliff period to the vesting schedule.BatcherConfidential: A batching primitive that enables routing between twoERC7984ERC20Wrappercontracts via a non-confidential route.
For convenience, this directory also includes:
VestingWalletConfidentialFactory: A factory which enables batch funding of vesting wallets.
Contracts
VestingWalletConfidential
VestingWalletCliffConfidential
VestingWalletConfidentialFactory
BatcherConfidential
import "@openzeppelin/confidential-contracts/contracts/finance/BatcherConfidential.sol";BatcherConfidential is a batching primitive that enables routing between two ERC7984ERC20Wrapper contracts
via a non-confidential route. Users deposit BatcherConfidential.fromToken into the batcher and receive BatcherConfidential.toToken in exchange. Deposits are
made by using ERC7984 transfer and call functions such as ERC7984.confidentialTransferAndCall.
Developers must implement the virtual function BatcherConfidential._executeRoute to perform the batch's route. This function is called
once the batch deposits are unwrapped into the underlying tokens. The function should swap the underlying BatcherConfidential.fromToken for
underlying BatcherConfidential.toToken. If an issue is encountered, the function should return ExecuteOutcome.Cancel to cancel the batch.
Developers must also implement the virtual function BatcherConfidential.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.
BatcherConfidential.toToken dust from rounding down will accumulate in the batcher over time.
The batcher does not support ERC7984ERC20Wrapper contracts prior to v0.4.0.
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.
The BatcherConfidential.toToken and BatcherConfidential.fromToken must be carefully inspected to ensure proper capacity is maintained. If BatcherConfidential.toToken or
BatcherConfidential.fromToken are filled--resulting in denial of service--batches could get bricked. The batcher would be unable to wrap
underlying tokens into BatcherConfidential.toToken. Further, if BatcherConfidential.fromToken is also filled, cancellation would also fail on rewrap.
Functions
- constructor(fromToken_, toToken_)
- claim(batchId, account)
- quit(batchId)
- dispatchBatch()
- dispatchBatchCallback(batchId, unwrapAmountCleartext, decryptionProof)
- onConfidentialTransferReceived(, from, amount, )
- fromToken()
- toToken()
- currentBatchId()
- unwrapRequestId(batchId)
- totalDeposits(batchId)
- deposits(batchId, account)
- exchangeRate(batchId)
- exchangeRateDecimals()
- routeDescription()
- batchState(batchId)
- _claim(batchId, account)
- _join(to, amount)
- _executeRoute(batchId, amount)
- _validateStateBitmap(batchId, allowedStates)
- _getAndIncreaseBatchId()
- encodeStateBitmap(batchState)
ReentrancyGuardTransient
Events
Errors
constructor(contract IERC7984ERC20Wrapper fromToken_, contract IERC7984ERC20Wrapper toToken_)
internal
#claim(uint256 batchId, address account) → euint64
public
#Claim the toToken corresponding to account's deposit in batch with id batchId.
This function is not gated and can be called by anyone. Claims could be frontrun.
quit(uint256 batchId) → euint64
public
#Quit the batch with id batchId. Entire deposit is returned to the user.
This can only be called if the batch has not yet been dispatched or if the batch was canceled.
Developers should consider adding additional restrictions to this function if maintaining confidentiality of deposits is critical to the application.
BatcherConfidential.dispatchBatch may fail if an incompatible version of ERC7984ERC20Wrapper is used.
This function must be unrestricted in cases where batch dispatching fails.
dispatchBatch()
public
#Permissionless function to dispatch the current batch. Increments the BatcherConfidential.currentBatchId.
Developers should consider adding additional restrictions to this function if maintaining confidentiality of deposits is critical to the application.
dispatchBatchCallback(uint256 batchId, uint64 unwrapAmountCleartext, bytes decryptionProof)
public
#Dispatch batch callback callable by anyone. This function finalizes the unwrap of BatcherConfidential.fromToken
and calls BatcherConfidential._executeRoute to perform the batch's route. If _executeRoute returns ExecuteOutcome.Partial,
this function should be called again with the same batchId, unwrapAmountCleartext, and decryptionProof.
onConfidentialTransferReceived(address, address from, euint64 amount, bytes) → ebool
external
#See IERC7984Receiver.onConfidentialTransferReceived.
Deposit BatcherConfidential.fromToken into the current batch.
See BatcherConfidential._claim to understand how the BatcherConfidential.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.
fromToken() → contract IERC7984ERC20Wrapper
public
#Batcher from token. Users deposit this token in exchange for BatcherConfidential.toToken.
toToken() → contract IERC7984ERC20Wrapper
public
#Batcher to token. Users receive this token in exchange for their BatcherConfidential.fromToken deposits.
currentBatchId() → uint256
public
#The ongoing batch id. New deposits join this batch.
unwrapRequestId(uint256 batchId) → bytes32
public
#The unwrap request id for a batch with id batchId.
totalDeposits(uint256 batchId) → euint64
public
#The total deposits made in batch with id batchId.
deposits(uint256 batchId, address account) → euint64
public
#The deposits made by account in batch with id batchId.
exchangeRate(uint256 batchId) → uint64
public
#The exchange rate set for batch with id batchId.
exchangeRateDecimals() → uint8
public
#The number of decimals of precision for the exchange rate.
routeDescription() → string
public
#Human readable description of what the batcher does.
batchState(uint256 batchId) → enum BatcherConfidential.BatchState
public
#Returns the current state of a batch. Reverts if the batch does not exist.
_claim(uint256 batchId, address account) → euint64
internal
#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.
_join(address to, euint64 amount) → euint64
internal
#Joins a batch with amount amount on behalf of to. Does not do any transfers in.
Returns the amount joined with.
_executeRoute(uint256 batchId, uint256 amount) → enum BatcherConfidential.ExecuteOutcome
internal
#Function which is executed by BatcherConfidential.dispatchBatchCallback after validation and unwrap finalization. The parameter
amount is the plaintext amount of the fromToken which were unwrapped--to attain the underlying tokens received,
evaluate amount * fromToken().rate(). This function should swap the underlying BatcherConfidential.fromToken for underlying BatcherConfidential.toToken.
This function returns an BatcherConfidential.ExecuteOutcome enum indicating the new state of the batch. If the route execution is complete,
the balance of the underlying BatcherConfidential.toToken is wrapped and the exchange rate is set.
BatcherConfidential.dispatchBatchCallback (and in turn BatcherConfidential._executeRoute) can be repeatedly called until the route execution is complete.
If a multi-step route is necessary, intermediate steps should return ExecuteOutcome.Partial. Intermediate steps must not
result in underlying BatcherConfidential.toToken being transferred into the batcher.
[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 ofBatcherConfidential.toTokenmust fit inuint64)
_validateStateBitmap(uint256 batchId, bytes32 allowedStates) → enum BatcherConfidential.BatchState
internal
#Check that the current state of a batch matches the requirements described by the allowedStates bitmap.
This bitmap should be built using _encodeStateBitmap.
If requirements are not met, reverts with a BatcherConfidential.BatchUnexpectedState error.
_getAndIncreaseBatchId() → uint256
internal
#Gets the current batch id and increments it.
encodeStateBitmap(enum BatcherConfidential.BatchState batchState) → bytes32
internal
#Encodes a BatchState into a bytes32 representation where each bit enabled corresponds to
the underlying position in the BatchState enum. For example:
0x000...1000 ^--- Canceled ^-- Finalized ^- Dispatched ^ Pending
BatchDispatched(uint256 indexed batchId)
event
#Emitted when a batch with id batchId is dispatched via BatcherConfidential.dispatchBatch.
BatchCanceled(uint256 indexed batchId)
event
#Emitted when a batch with id batchId is canceled.
BatchFinalized(uint256 indexed batchId, uint64 exchangeRate)
event
#Emitted when a batch with id batchId is finalized with an exchange rate of exchangeRate.
Joined(uint256 indexed batchId, address indexed account, euint64 amount)
event
#Emitted when an account joins a batch with id batchId with a deposit of amount.
Claimed(uint256 indexed batchId, address indexed account, euint64 amount)
event
#Emitted when an account claims their amount from batch with id batchId.
Quit(uint256 indexed batchId, address indexed account, euint64 amount)
event
#Emitted when an account quits a batch with id batchId.
BatchNonexistent(uint256 batchId)
error
#The batchId does not exist. Batch IDs start at 1 and must be less than or equal to BatcherConfidential.currentBatchId.
ZeroDeposits(uint256 batchId, address account)
error
#The account has a zero deposits in batch batchId.
BatchUnexpectedState(uint256 batchId, enum BatcherConfidential.BatchState current, bytes32 expectedStates)
error
#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.
InvalidExchangeRate(uint256 batchId, uint256 totalDeposits, uint64 exchangeRate)
error
#Thrown when the given exchange rate is invalid. The exchange rate must be non-zero and the wrapped
amount of BatcherConfidential.toToken must be less than or equal to type(uint64).max.
Unauthorized()
error
#The caller is not authorized to call this function.
InvalidWrapperToken(address token)
error
#The given token does not support IERC7984ERC20Wrapper via ERC165.
import "@openzeppelin/confidential-contracts/contracts/finance/ERC7821WithExecutor.sol";Extension of ERC7821 that adds an ERC7821WithExecutor.executor address that is able to execute arbitrary calls via ERC7821.execute.
Functions
Events
Initializable
Errors
ERC7821
Initializable
executor() → address
public
#Trusted address that is able to execute arbitrary calls from the vesting wallet via ERC7821.execute.
_ERC7821WithExecutor_init(address executor)
internal
#_erc7821AuthorizedExecutor(address caller, bytes32 mode, bytes executionData) → bool
internal
#Access control mechanism for the execute function.
By default, only the contract itself is allowed to execute.
Override this function to implement custom access control, for example to allow the ERC-4337 entrypoint to execute.
function _erc7821AuthorizedExecutor(
address caller,
bytes32 mode,
bytes calldata executionData
) internal view virtual override returns (bool) {
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
}import "@openzeppelin/confidential-contracts/contracts/finance/VestingWalletCliffConfidential.sol";An extension of VestingWalletConfidential that adds a cliff to the vesting schedule. The cliff is cliffSeconds long and
starts at the vesting start timestamp (see VestingWalletConfidential).
Functions
- cliff()
- __VestingWalletCliffConfidential_init(beneficiary, startTimestamp, durationSeconds, cliffSeconds)
- __VestingWalletCliffConfidential_init_unchained(cliffSeconds)
- _vestingSchedule(totalAllocation, timestamp)
VestingWalletConfidential
ReentrancyGuardTransient
OwnableUpgradeable
ContextUpgradeable
Events
VestingWalletConfidential
OwnableUpgradeable
Initializable
Errors
ReentrancyGuardTransient
OwnableUpgradeable
Initializable
cliff() → uint64
public
#The timestamp at which the cliff ends.
__VestingWalletCliffConfidential_init(address beneficiary, uint48 startTimestamp, uint48 durationSeconds, uint48 cliffSeconds)
internal
#Set the duration of the cliff, in seconds. The cliff starts at the vesting
start timestamp (see VestingWalletConfidential.start) and ends cliffSeconds later.
__VestingWalletCliffConfidential_init_unchained(uint48 cliffSeconds)
internal
#_vestingSchedule(euint128 totalAllocation, uint48 timestamp) → euint128
internal
#This function returns the amount vested, as a function of time, for
an asset given its total historical allocation. Returns 0 if the VestingWalletCliffConfidential.cliff timestamp is not met.
The cliff not only makes the schedule return 0, but it also ignores every possible side
effect from calling the inherited implementation (i.e. super._vestingSchedule). Carefully consider
this caveat if the overridden implementation of this function has any (e.g. writing to memory or reverting).
VestingWalletCliffConfidentialInvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds)
error
#The specified cliff duration is larger than the vesting duration.
import "@openzeppelin/confidential-contracts/contracts/finance/VestingWalletConfidential.sol";A vesting wallet is an ownable contract that can receive ERC7984 tokens, and release these assets to the wallet owner, also referred to as "beneficiary", according to a vesting schedule.
Any assets transferred to this contract will follow the vesting schedule as if they were locked from the beginning. Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) be immediately releasable.
By setting the duration to 0, one can configure this contract to behave like an asset timelock that holds tokens for a beneficiary until a specified time.
Since the wallet is Ownable, and ownership can be transferred, it is possible to sell unvested tokens.
When using this contract with any token whose balance is adjusted automatically (i.e. a rebase token), make sure to account for the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended.
Confidential vesting wallet contracts can be deployed (as clones) using the VestingWalletConfidentialFactory.
Functions
- start()
- duration()
- end()
- released(token)
- releasable(token)
- release(token)
- vestedAmount(token, timestamp)
- __VestingWalletConfidential_init(beneficiary, startTimestamp, durationSeconds)
- __VestingWalletConfidential_init_unchained(startTimestamp, durationSeconds)
- _vestingSchedule(totalAllocation, timestamp)
ReentrancyGuardTransient
OwnableUpgradeable
ContextUpgradeable
Events
OwnableUpgradeable
Initializable
Errors
ReentrancyGuardTransient
OwnableUpgradeable
Initializable
start() → uint64
public
#Timestamp at which the vesting starts.
duration() → uint64
public
#Duration of the vesting in seconds.
end() → uint64
public
#Timestamp at which the vesting ends.
released(address token) → euint128
public
#Amount of token already released
releasable(address token) → euint64
public
#Getter for the amount of releasable token tokens. token should be the address of an
IERC7984 contract.
release(address token)
public
#Release the tokens that have already vested.
Emits a VestingWalletConfidential.VestingWalletConfidentialTokenReleased event.
vestedAmount(address token, uint48 timestamp) → euint128
public
#Calculates the amount of tokens that have been vested at the given timestamp. Default implementation is a linear vesting curve.
__VestingWalletConfidential_init(address beneficiary, uint48 startTimestamp, uint48 durationSeconds)
internal
#Initializes the vesting wallet for a given beneficiary with a start time of startTimestamp
and an end time of startTimestamp + durationSeconds.
__VestingWalletConfidential_init_unchained(uint48 startTimestamp, uint48 durationSeconds)
internal
#_vestingSchedule(euint128 totalAllocation, uint48 timestamp) → euint128
internal
#This returns the amount vested, as a function of time, for an asset given its total historical allocation.
VestingWalletConfidentialTokenReleased(address indexed token, euint64 amount)
event
#Emitted when releasable vested tokens are released.
import "@openzeppelin/confidential-contracts/contracts/finance/VestingWalletConfidentialFactory.sol";A factory which enables batch funding of vesting wallets.
The VestingWalletConfidentialFactory._deployVestingWalletImplementation, VestingWalletConfidentialFactory._initializeVestingWallet, and VestingWalletConfidentialFactory._validateVestingWalletInitArgs
functions remain unimplemented to allow for custom implementations of the vesting wallet to be used.
Functions
- constructor()
- batchFundVestingWalletConfidential(token, vestingPlans, inputProof)
- createVestingWalletConfidential(initArgs)
- predictVestingWalletConfidential(initArgs)
- _validateVestingWalletInitArgs(initArgs)
- _initializeVestingWallet(vestingWalletAddress, initArgs)
- _deployVestingWalletImplementation()
- _getCreate2VestingWalletConfidentialSalt(initArgs)
Events
constructor()
internal
#batchFundVestingWalletConfidential(address token, struct VestingWalletConfidentialFactory.VestingPlan[] vestingPlans, bytes inputProof)
public
#Batches the funding of multiple confidential vesting wallets.
Funds are sent to deterministic wallet addresses. Wallets can be created either before or after this operation.
Emits a VestingWalletConfidentialFactory.VestingWalletConfidentialFunded event for each funded vesting plan.
createVestingWalletConfidential(bytes initArgs) → address
public
#Creates a confidential vesting wallet.
Emits a VestingWalletConfidentialFactory.VestingWalletConfidentialCreated.
predictVestingWalletConfidential(bytes initArgs) → address
public
#Predicts the deterministic address for a confidential vesting wallet.
_validateVestingWalletInitArgs(bytes initArgs)
internal
#Virtual function that must be implemented to validate the initArgs bytes.
_initializeVestingWallet(address vestingWalletAddress, bytes initArgs)
internal
#Virtual function that must be implemented to initialize the vesting wallet at vestingWalletAddress.
_deployVestingWalletImplementation() → address
internal
#Internal function that is called once to deploy the vesting wallet implementation.
Vesting wallet clones will be initialized by calls to the VestingWalletConfidentialFactory._initializeVestingWallet function.
_getCreate2VestingWalletConfidentialSalt(bytes initArgs) → bytes32
internal
#Gets create2 salt for a confidential vesting wallet.
VestingWalletConfidentialFunded(address indexed vestingWalletConfidential, address indexed token, euint64 transferredAmount, bytes initArgs)
event
#Emitted for each vesting wallet funded within a batch.
VestingWalletConfidentialCreated(address indexed vestingWalletConfidential, bytes initArgs)
event
#Emitted when a vesting wallet is deployed.