Universal Balance
Overview
The Universal Balance system is a user-facing token management layer within the Curvance Protocol, providing a flexible interface for users to manage their assets. It enables seamless transitions between idle holdings and yield-generating positions within Curvance's ecosystem, all while maintaining non-custodial control.
The system consists of two main contracts:
UniversalBalance: Core implementation for ERC20 tokens.
UniversalBalanceNative: Extension that adds support for native gas tokens (ETH, BERA, etc.).
Core Concepts
Dual Balance System
Universal Balance introduces a dual-balance accounting model for each user:
Sitting Balance: Tokens held in the contract but not deployed to lending markets.
Lent Balance: Tokens deployed into Curvance's lending markets to earn yield.
This dual-state approach allows users to maintain instant liquidity for a portion of their funds while earning yield on another portion, all through a single interface.
Architecture
Core Components
UniversalBalance: Base implementation for ERC20 tokens.
UniversalBalanceNative: Extension for native gas tokens with wrapping/unwrapping.
EToken: Corresponding lending market token that generates yield.
CentralRegistry: Provides system-wide configuration and permissions.
State Management
Universal Balance maintains the following key state variables:
struct UserBalance {
uint256 sittingBalance;
uint256 lentBalance;
}
// User balances tracking
mapping(address => UserBalance) public userBalances;
// Contract configuration
IEToken public immutable linkedToken;
address public immutable underlying;
Each Universal Balance contract is linked to:
A specific underlying token (e.g., USDC).
The corresponding EToken in Curvance (e.g., eUSDC).
Data Flows
Deposit Flow
User deposits tokens to UniversalBalance via
deposit()
ordepositFor()
.User specifies whether to keep as sitting balance or lend it.
If lending is chosen:
Tokens are transferred to the EToken contract via
mint()
.Resulting EToken shares are tracked in the user's
lentBalance
.
If keeping as sitting balance:
Tokens are held in the
UniversalBalance
contract.User's
sittingBalance
is increased.
Withdrawal Flow
User requests withdrawal via
withdraw()
orwithdrawFor()
.Contract checks if withdrawal can be fulfilled from sitting balance.
If sitting balance is insufficient:
Required ETokens are redeemed from lending market.
Underlying tokens are received.
Tokens are sent to the recipient.
User's balance (sitting or lent) is reduced accordingly.
Balance Conversion Flow
User requests to lend sitting balance via
lendAssets()
.Sitting balance is reduced.
Tokens are deployed to lending market.
Lent balance is increased.
User requests to unlend via
unlendAssets()
.Lent balance is reduced.
ETokens are redeemed from lending market.
Sitting balance is increased.
Native Token Flow (UniversalBalanceNative)
User sends native tokens (ETH) directly to contract.
Native tokens are wrapped (WETH).
Added to user's sitting balance.
User deposits native tokens via
depositNative()
.Similar to standard deposit with auto-wrapping.
User withdraws to native tokens via
withdrawNative()
.Wrapped tokens are unwrapped.
Native tokens are sent to recipient.
Integration with Curvance Ecosystem
EToken Interaction
Universal Balance contracts interact directly with their linked EToken contracts.
When lending, they call
mint()
on the EToken.When unlending, they call
redeem()
on the EToken.Yield accrues automatically through the EToken's interest model.
Oracle Integration
UniversalBalanceNative includes special support for oracle funding:
Oracle Manager can request funds via
useBalanceForOracleUpdate()
.User's balance is used to pay for oracle updates.
This enables "pull-based" oracle updates where users can fund oracle operations.
Security Features
Permission System: Implements delegated operations through the
PluginDelegable
contract.Reentrancy Protection: All critical functions include reentrancy guards.
Non-Custodial Design: Users maintain full control of their assets.
Permission Checks: Actions that affect user balances verify permissions.
Batch Operations: Multi-user functions to reduce gas costs and transaction volume.
User Features
Social Elements
Direct Transfers: Transfer portions of balance to other users.
Permission Delegation: Allow third-party operations on your balance.
Batch Operations: Efficient multi-user management.
Flexibility
Dynamic Allocation: Freely shift between sitting and lent states.
Multiple Recipients: Deposit or withdraw to different addresses.
Mixed Source Withdrawals: Pull from sitting first, then lent as needed.
Contract Interactions
UniversalBalance interacts with several Curvance contracts:

UniversalBalanceNative adds these interactions:

Implementation Notes
Each UniversalBalance contract is token-specific, managing only one underlying asset.
Deposit and withdrawal functions include options for immediate lending.
The system tracks balances via accounting rather than transferring tokens to users.
For native token operations, wrapping/unwrapping occurs transparently to the user.
User Interaction Functions
UniversalBalance Functions
deposit()
Description: Deposits underlying token into user's Universal Balance account, either to be held or lent out.
Contract: UniversalBalance
Function signature:
function deposit(uint256 amount, bool willLend) external
uint256
amount
The amount of underlying token to be deposited.
bool
willLend
Whether the deposited underlying tokens should be lent out inside Curvance Protocol.
Events:
// From UniversalBalance
event Deposit(
address indexed by,
address indexed owner,
uint256 assets,
bool lendingDeposit
)
// From Underlying Asset & EToken
// Potentially emitted multiple times:
// 1. Always emitted when assets are transferred to UniversalBalance
// 2. If willLend is true, transferring underlying asset from UniversalBalance to EToken
// 3. Emitted when ETokens are minted.
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
depositFor()
Description: Deposits underlying token into recipient's Universal Balance account, either to be held or lent out. Requires that recipient has approved the caller previously to access their Universal Balance.
Contract: UniversalBalance
Function signature:
function depositFor(uint256 amount, bool willLend, address recipient) external
uint256
amount
The amount of underlying token to be deposited.
bool
willLend
Whether the deposited underlying tokens should be lent out inside Curvance Protocol.
address
recipient
The account who will receive the deposit.
Events:
// From UniversalBalance
event Deposit(
address indexed by,
address indexed owner,
uint256 assets,
bool lendingDeposit
)
// From Underlying Asset & EToken
// Potentially emitted multiple times:
// 1. Always emitted when assets are transferred to UniversalBalance
// 2. If willLend is true, transferring underlying asset from UniversalBalance to EToken
// 3. Emitted when ETokens are minted.
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
multiDepositFor()
Description: Deposits underlying token into recipients Universal Balance accounts, either to be held or lent out. Requires that all recipients have approved the caller previously to access their Universal Balance.
Contract: UniversalBalance
Function signature:
function multiDepositFor(
uint256 depositSum,
uint256[] calldata amounts,
bool[] calldata willLend,
address[] calldata recipients
) external
uint256
depositSum
The total sum of underlying tokens being deposited.
uint256[]
amounts
An array containing the amount of underlying token to be deposited to each account.
bool[]
willLend
An array containing whether the deposited underlying tokens should be lent out inside Curvance Protocol for each account.
address[]
recipients
An array containing the accounts who will receive a deposit based on their matching amounts value.
Events:
For each deposit:
// From UniversalBalance
event Deposit(
address indexed by,
address indexed owner,
uint256 assets,
bool lendingDeposit
)
// From Underlying Asset & EToken
// Potentially emitted multiple times:
// 1. Always emitted when assets are transferred to UniversalBalance
// 2. If willLend is true, transferring underlying asset from UniversalBalance to EToken
// 3. Emitted when ETokens are minted.
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted once per transaction.
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
withdraw()
Description: Withdraws underlying token from user's Universal Balance account, currently held or lent out.
Contract: UniversalBalance
Function signature:
function withdraw(
uint256 amount,
bool forceLentRedemption,
address recipient
) external returns (uint256 amountWithdrawn, bool lendingBalanceUsed)
uint256
amount
The amount of underlying token to be withdrawn.
bool
forceLentRedemption
Whether the withdrawn underlying tokens should be pulled only from owner's lent position or the full account.
address
recipient
The account who will receive the underlying assets.
Return data:
uint256
The amount of underlying token actually withdrawn.
bool
Whether lent balance was used for the withdrawal.
Events:
// From UniversalBalance
event Withdraw(
address indexed by,
address indexed to,
address indexed owner,
uint256 assets,
bool lendingRedemption
)
// From EToken & Underlying asset.
// Always emitted from underlying asset when transferring to the recipient.
// If withdrawing from lent balance:
// Emitted from the EToken when burning tokens during redemption.
// Emitted from the underlying asset when transferring from EToken to Universal Balance.
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
// From MarketManager
// Potentially emitted if positions need to be closed.
event PositionAdjusted(address mToken, address account, bool open);
withdrawFor
Description: Withdraws underlying token from owner's Universal Balance account, currently held or lent out. Requires that owner has approved the caller previously to access their Universal Balance.
Contract: UniversalBalance
Function signature:
function withdrawFor(
uint256 amount,
bool forceLentRedemption,
address recipient,
address owner
) external returns (uint256 amountWithdrawn, bool lendingBalanceUsed)
uint256
amount
The amount of underlying token to be withdrawn.
bool
forceLentRedemption
Whether the withdrawn underlying tokens should be pulled only from owner's lent position or the full account.
address
recipient
The account who will receive the underlying assets.
address
owner
The account that will redeem from their universal balance.
Return data:
uint256
The amount of underlying token actually withdrawn.
bool
Whether lent balance was used for the withdrawal..
Events:
event Withdraw(
address indexed by,
address indexed to,
address indexed owner,
uint256 assets,
bool lendingRedemption
)
// From EToken & Underlying asset.
// Always emitted from underlying asset when transferring to the recipient.
// If withdrawing from lent balance:
// Emitted from the EToken when burning tokens during redemption.
// Emitted from the underlying asset when transferring from EToken to Universal Balance.
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
// From MarketManager
// Potentially emitted if positions need to be closed.
event PositionAdjusted(address mToken, address account, bool open);
multiWithdrawFor
Description: Withdraws underlying token from owners Universal Balance accounts, currently held or lent out. Requires that each owners has approved the caller previously to access their Universal Balance.
Contract: UniversalBalance
Function signature:
function multiWithdrawFor(
uint256[] calldata amounts,
bool[] calldata forceLentRedemption,
address recipient,
address[] calldata owners
) external
uint256[]
amounts
An array containing the amount of underlying token to be withdrawn from each account.
bool[]
forceLentRedemption
An array containing whether the withdrawn underlying tokens should be pulled only from an owners lent position or the full account.
address
recipient
The account who will receive the underlying assets.
address[]
owners
An array containing the accounts that will redeem from their Universal Balance.
Events:
For each withdrawal:
event Withdraw(
address indexed by,
address indexed to,
address indexed owner,
uint256 assets,
bool lendingRedemption
)
// From EToken & Underlying asset.
// Always emitted from underlying asset when transferring to the recipient.
// If withdrawing from lent balance:
// Emitted from the EToken when burning tokens during redemption.
// Emitted from the underlying asset when transferring from EToken to Universal Balance.
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted once per transaction.
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
// From MarketManager
// Potentially emitted if positions need to be closed.
event PositionAdjusted(address mToken, address account, bool open);
shiftBalance()
Description: Moves a user's Universal Balance between lent and sitting mode.
Contract: UniversalBalance
Function signature:
function shiftBalance(
uint256 amount,
bool fromLent
) external returns (uint256 amountWithdrawn, bool lendingBalanceUsed)
uint256
amount
The amount of underlying token to be shifted.
bool
fromLent
Whether the shifted underlying tokens should be pulled from the user's lent balance or the sitting balance.
Return data:
uint256
The amount of underlying token actually shifted
bool
Whether lent balance was used for the operation
Events:
// From UniversalBalance
event Withdraw(
address indexed by,
address indexed to,
address indexed owner,
uint256 assets,
bool lendingRedemption
)
event Deposit(
address indexed by,
address indexed owner,
uint256 assets,
bool lendingDeposit
)
// From EToken
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
// 1. Emitted when burning tokens during redemption if shifting from lent balance
// 2. Emitted from the underlying token when transferring from EToken to UniversalBalance
// 3. Emitted from underlying token is transferred from UniversalBalance to the Etoken if willLend is true
event Transfer(address indexed from, address indexed to, uint256 value);
// Potentially emitted from MarketManager if positions need to be closed
event PositionAdjusted(address mToken, address account, bool open);
transfer()
Description: Transfers amount from caller's Universal Balance, currently held or lent out to recipient.
Contract: UniversalBalance
Function signature:
function transfer(
uint256 amount,
bool forceLentRedemption,
bool willLend,
address recipient
) external returns (uint256 amountTransferred, bool lendingBalanceUsed)
uint256
amount
The amount of underlying token to be transferred.
bool
forceLentRedemption
Whether the transferred underlying tokens should be pulled only from the caller's lent position or the full account.
bool
willLend
Whether the deposited underlying tokens should be lent out inside Curvance Protocol.
address
recipient
The account who will receive the transferred balance.
Return data:
uint256
The amount of underlying token actually transferred.
bool
Whether lent balance was used for the transfer.
Events:
// From UniversalBalance
event Withdraw(
address indexed by,
address indexed to,
address indexed owner,
uint256 assets,
bool lendingRedemption
)
event Deposit(
address indexed by,
address indexed owner,
uint256 assets,
bool lendingDeposit
)
// From EToken
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
// 1. Emitted when burning tokens during redemption if shifting from lent balance
// 2. Emitted from the underlying token when transferring from EToken to UniversalBalance
// 3. Emitted from underlying token is transferred from UniversalBalance to the Etoken if willLend is true
event Transfer(address indexed from, address indexed to, uint256 value);
// Potentially emitted from MarketManager if positions need to be closed
event PositionAdjusted(address mToken, address account, bool open);
transferFor()
Description: Transfers amount from owner's Universal Balance, currently held or lent out to recipient. Requires that owner has approved the caller previously to access their Universal Balance.
Contract: UniversalBalance
Function signature:
function transferFor(
uint256 amount,
bool forceLentRedemption,
bool willLend,
address recipient,
address owner
) external returns (uint256 amountTransferred, bool lendingBalanceUsed)
uint256
amount
The amount of underlying token to be transferred.
bool
forceLentRedemption
Whether the transferred underlying tokens should be pulled only from owner's lent position or the full account.
bool
willLend
Whether the deposited underlying tokens should be lent out inside Curvance Protocol.
address
recipient
The account who will receive the transferred balance.
address
owner
The account that will transfer from their universal balance.
Return data:
uint256
The amount of underlying token actually transferred.
bool
Whether lent balance was used for the transfer.
Events:
// From UniversalBalance
event Withdraw(
address indexed by,
address indexed to,
address indexed owner,
uint256 assets,
bool lendingRedemption
)
event Deposit(
address indexed by,
address indexed owner,
uint256 assets,
bool lendingDeposit
)
// From EToken
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
// 1. Emitted when burning tokens during redemption if shifting from lent balance
// 2. Emitted from the underlying token when transferring from EToken to UniversalBalance
// 3. Emitted from underlying token is transferred from UniversalBalance to the Etoken if willLend is true
event Transfer(address indexed from, address indexed to, uint256 value);
// Potentially emitted from MarketManager if positions need to be closed
event PositionAdjusted(address mToken, address account, bool open);
UniversalBalanceNative Functions
depositNative()
Description: Deposits native gas token into user's Universal Balance account, either to be held or lent out.
Contract: UniversalBalanceNative
Function signature:
function depositNative(bool isLent) external payable
bool
isLent
Whether the deposited native tokens should be lent out inside Curvance Protocol (as wrapped native).
Events:
// From UniversalBalanceNative
event Deposit(
address indexed by,
address indexed owner,
uint256 assets,
bool lendingDeposit
)
// Possibly emitted from the wrapped native asset
event Deposit(address from, address to, uint256 value);
// From EToken
// 1. Emitted if isLent is true
// 2. Emitted from the eToken when tokens are minted to UniversalBalance
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
depositNativeFor()
Description: Deposits native gas token into recipient's Universal Balance account, either to be held or lent out. Requires that recipient has approved the caller previously to access their Universal Balance.
Contract: UniversalBalanceNative
Function signature:
function depositNativeFor(
bool isLent,
address recipient
) external payable
bool
isLent
Whether the deposited native tokens should be lent out inside Curvance Protocol (as wrapped native).
address
recipient
The account who will receive the deposit.
Events:
// From UniversalBalanceNative
event Deposit(
address indexed by,
address indexed owner,
uint256 assets,
bool lendingDeposit
)
// Possibly emitted from the wrapped native asset
event Deposit(address from, address to, uint256 value);
// From EToken
// 1. Emitted if isLent is true
// 2. Emitted from the eToken when tokens are minted to UniversalBalance
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
multiDepositNativeFor()
Description: Deposits native gas token into recipient's Universal Balance account, either to be held or lent out. Requires that all recipients have approved the caller previously to access their Universal Balance.
Contract: UniversalBalanceNative
Function signature:
function multiDepositNativeFor(
uint256[] calldata amounts,
bool[] calldata willLend,
address[] calldata recipients
) external payable
uint256[]
amounts
An array containing the amount of native token to be deposited to each account.
bool[]
willLend
An array containing whether the deposited native tokens should be lent out inside Curvance Protocol for each account.
address[]
recipients
An array containing the accounts who will receive a deposit based on their matching amounts value.
Events:
// From UniversalBalanceNative
event Deposit(
address indexed by,
address indexed owner,
uint256 assets,
bool lendingDeposit
)
// Possibly emitted from the wrapped native asset
event Deposit(address from, address to, uint256 value);
// From EToken
// 1. Emitted if isLent is true
// 2. Emitted from the eToken when tokens are minted to UniversalBalance
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted once per transaction.
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
withdrawNative()
Description: Withdraws wrapped native token from user's Universal Balance account, either currently held or lent out and transfers it to the user in native form.
Contract: UniversalBalanceNative
Function signature:
function withdrawNative(
uint256 amount,
bool forceLentRedemption,
address recipient
) external returns (uint256 amountWithdrawn, bool lendingBalanceUsed)
uint256
amount
The amount of native token to be withdrawn.
bool
forceLentRedemption
Whether the withdrawn underlying tokens should be pulled only from owner's lent position or the full account.
address
recipient
The account who will receive the underlying assets.
Return data:
uint256
The amount of native token actually withdrawn.
bool
Whether lent balance was used for the withdrawal.
Events:
// From UniversalBalanceNative
event Withdraw(
address indexed by,
address indexed to,
address indexed owner,
uint256 assets,
bool lendingRedemption
)
// From the wrapped native asset
event Withdrawal(address user, uint256 amount);
// From EToken
// 1. Emitted when burning tokens during redemption.
// 2. Emitted from the wrapped native asset.
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
// From MarketManager
// Potentially emitted if positions need to be closed.
event PositionAdjusted(address mToken, address account, bool open);
withdrawNativeFor()
Description: Withdraws wrapped native token from owner's universal balance account, either currently held or lent out and transfers it to the user in native form. Requires that owner has approved the caller previously to access their Universal Balance.
Contract: UniversalBalanceNative
Function signature:
function withdrawNativeFor(
uint256 amount,
bool forceLentRedemption,
address recipient,
address owner
) external returns (uint256 amountWithdrawn, bool lendingBalanceUsed)
uint256
amount
The amount of native token to be withdrawn.
bool
forceLentRedemption
Whether the withdrawn underlying tokens should be pulled only from owner's lent position or the full account.
address
recipient
The account who will receive the native token.
address
owner
The account that will redeem from their universal balance.
Return data:
uint256
The amount of native token actually withdrawn.
bool
Whether lent balance was used for the withdrawal.
Events:
// From UniversalBalanceNative
event Withdraw(
address indexed by,
address indexed to,
address indexed owner,
uint256 assets,
bool lendingRedemption
)
// From the wrapped native asset
event Withdrawal(address user, uint256 amount);
// From EToken
// 1. Emitted when burning tokens during redemption.
// 2. Emitted from the wrapped native asset.
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
// From MarketManager
// Potentially emitted if positions need to be closed.
event PositionAdjusted(address mToken, address account, bool open);
multiWithdrawNativeFor()
Description: Withdraws native gas token from owners Universal Balance accounts, currently held or lent out. Requires that each owners has approved the caller previously to access their Universal Balance.
Contract: UniversalBalanceNative
Function signature:
function multiWithdrawNativeFor(
uint256[] calldata amounts,
bool[] calldata forceLentRedemption,
address recipient,
address[] calldata owners
) external
uint256[]
amounts
An array containing the amount of native token to be withdrawn from each account.
bool[]
forceLentRedemption
An array containing whether the withdrawn underlying tokens should be pulled only from an owners lent position or the full account.
address
recipient
The account who will receive the native assets.
address[]
owners
An array containing the accounts that will redeem from their Universal Balance.
Events:
For every withdrawal:
// From UniversalBalanceNative
event Withdraw(
address indexed by,
address indexed to,
address indexed owner,
uint256 assets,
bool lendingRedemption
)
// From the wrapped native asset
event Withdrawal(address user, uint256 amount);
// From EToken
// 1. Emitted when burning tokens during redemption.
// 2. Emitted from the wrapped native asset.
event Transfer(address indexed from, address indexed to, uint256 value);
// Only emitted once per transaction.
// Only emitted if interest needs to be accrued.
event InterestAccrued(
uint256 debtAccumulated,
uint256 exchangeRate,
uint256 totalBorrows
);
// From MarketManager
// Potentially emitted if positions need to be closed.
event PositionAdjusted(address mToken, address account, bool open);
Last updated