Diffusal

DiffusalPortfolioManager

Portfolio management for gas-bounded cross-margin liquidations

The DiffusalPortfolioManager contract manages user portfolios for gas-bounded cross-margin liquidations. Each user can have multiple portfolios, with each portfolio having a maximum of 16 positions. Cross-margin applies within each portfolio only.


Overview

The portfolio manager enables users to organize their positions into separate portfolios:

FunctionDescription
Multiple PortfoliosEach user can create multiple portfolios
Position LimitsMax 16 positions per portfolio for gas-bounded liquidation
Cross-Margin ScopeCross-margin applies within each portfolio only
Position TransferMove positions between portfolios
Auto-CreationDefault portfolio (0) auto-created on first deposit

Why Portfolios?

The primary motivation for portfolios is gas-bounded liquidation:

ProblemSolution
Unbounded position iterationsMax 16 positions per portfolio
Gas-expensive liquidationEach portfolio is independent liquidation unit
Single margin pool riskIsolated risk across portfolios

UX Flow

Normal Users

  1. Call deposit() on CollateralVault
  2. System auto-creates portfolio 0 (default)
  3. All trades go to portfolio 0
  4. Cross-margin across up to 16 positions

Power Users

  1. Use default portfolio 0 until hitting 16-position limit
  2. Create additional portfolios via depositToPortfolio()
  3. Organize positions strategically across portfolios
  4. Transfer positions between portfolios as needed

Storage & State

/// @custom:storage-location erc7201:diffusal.storage.PortfolioManager
struct PortfolioManagerStorage {
    address owner;                                           // Contract owner
    address pendingOwner;                                    // Two-step ownership transfer
    address positionManager;                                 // PositionManager reference
    address vault;                                           // CollateralVault reference
    address marginCalculator;                                // MarginCalculator reference (health checks)
    mapping(address => bool) operators;                      // Authorized operators
    mapping(address => uint256[]) userPortfolios;            // User -> portfolio IDs
    mapping(address => mapping(uint256 => bool)) portfolioExists; // User -> portfolioId -> exists
    mapping(address => uint256) nextPortfolioId;             // User -> next ID to assign
    mapping(address => mapping(uint256 => uint256)) portfolioIndex; // User -> portfolioId -> array index
}

External Functions

Portfolio Management

createPortfolio

Creates a new portfolio for a user. Only callable by operators.

function createPortfolio(address user) external returns (uint256 portfolioId)
ParameterTypeDescription
useraddressUser to create a portfolio for
ReturnTypeDescription
portfolioIduint256ID of the newly created portfolio

Behavior:

  • Assigns the next available portfolio ID for the user
  • Marks portfolio as existing
  • Adds to user's portfolio list
  • Increments counter for next portfolio

Emits: PortfolioCreated(user, portfolioId)


deletePortfolio

Deletes an empty portfolio. Only callable by operators.

function deletePortfolio(address user, uint256 portfolioId) external
ParameterTypeDescription
useraddressUser who owns the portfolio
portfolioIduint256ID of portfolio to delete

Requirements:

  • Portfolio must exist
  • Portfolio must be empty (no positions, no collateral)

Note: Any empty portfolio can be deleted, including portfolio 0 (the default portfolio).

Emits: PortfolioDeleted(user, portfolioId)


transferPosition

Transfers a position between portfolios. Only callable by operators.

function transferPosition(
    address user,
    uint256 fromPortfolioId,
    uint256 toPortfolioId,
    bytes32 seriesId,
    int256 amount
) external
ParameterTypeDescription
useraddressUser who owns both portfolios
fromPortfolioIduint256Source portfolio ID
toPortfolioIduint256Destination portfolio ID
seriesIdbytes32Series of the position to transfer
amountint256Amount to transfer (can be partial)

Behavior:

  • Validates both portfolios exist
  • Removes position from source portfolio
  • Adds position to destination portfolio
  • Validates margin health on both portfolios

Emits: PositionTransferred(user, fromPortfolioId, toPortfolioId, seriesId, amount)


View Functions

getUserPortfolios

Returns all portfolio IDs for a user.

function getUserPortfolios(address user) external view returns (uint256[] memory portfolioIds)

portfolioExists

Checks if a portfolio exists.

function portfolioExists(address user, uint256 portfolioId) external view returns (bool exists)

getPortfolioCount

Returns the number of portfolios a user has.

function getPortfolioCount(address user) external view returns (uint256 count)

getNextPortfolioId

Returns the next portfolio ID that will be assigned for a user.

function getNextPortfolioId(address user) external view returns (uint256 nextId)

isOperator

Checks if an address is an authorized operator.

function isOperator(address operator) external view returns (bool authorized)

marginCalculator

Returns the margin calculator address.

function marginCalculator() external view returns (address)

paused

Returns whether the contract is paused.

function paused() external view returns (bool)

Owner Functions

setOperator

Authorizes or revokes an operator.

function setOperator(address operator, bool authorized) external

Authorized operators:

  • DiffusalCollateralVault (creates portfolios on first deposit)
  • DiffusalOptionsPositionManager (tracks positions per portfolio)

Emits: OperatorUpdated(operator, authorized)


setPositionManager

Sets the position manager address.

function setPositionManager(address _positionManager) external

Emits: PositionManagerUpdated(_positionManager)


setVault

Sets the collateral vault address.

function setVault(address _vault) external

Emits: CollateralVaultUpdated(_vault)


setMarginCalculator

Sets the margin calculator address.

function setMarginCalculator(address _marginCalculator) external

Emits: MarginCalculatorUpdated(oldCalculator, newCalculator)


pause / unpause

Pauses or unpauses the contract.

function pause() external
function unpause() external

transferOwnership / acceptOwnership

Two-step ownership transfer pattern.

function transferOwnership(address newOwner) external
function acceptOwnership() external

Flow:

  1. Current owner calls transferOwnership(newOwner)
  2. New owner calls acceptOwnership() to complete transfer

Emits: OwnershipTransferStarted, OwnershipTransferred


Events

EventParametersDescription
PortfolioCreateduser, portfolioIdNew portfolio created
PortfolioDeleteduser, portfolioIdPortfolio deleted
PositionTransferreduser, fromPortfolioId, toPortfolioId, seriesId, amountPosition moved between portfolios
OperatorUpdatedoperator, authorizedOperator status changed
PositionManagerUpdatedpositionManagerPosition manager address updated
CollateralVaultUpdatedvaultCollateral vault address updated
MarginCalculatorUpdatedoldCalculator, newCalculatorMargin calculator address changed
OwnershipTransferStartedpreviousOwner, newOwnerOwnership transfer initiated
OwnershipTransferredpreviousOwner, newOwnerOwnership transferred

Portfolio-Aware System Flow

Deposit Flow

┌────────────────────────────────────────────┐
│  User calls deposit() on CollateralVault   │
└──────────────────────┬─────────────────────┘

               ┌─────────────────┐
               │  First deposit? │
               └────────┬────────┘
      Yes ┌───────────┴───────────┐ No
              ▼                    ▼
┌───────────────────────┐  ┌───────────────────────┐
│ CollateralVault       │  │ Deposit to existing   │
│ calls createPortfolio │  │ portfolio             │
│ () - Creates          │  └───────────┬───────────┘
│ portfolio 0           │              │
└───────────┬───────────┘              │
            └─────────────┬────────────┘

┌────────────────────────────────────────────┐
│  Collateral credited to user's portfolio   │
└────────────────────────────────────────────┘

Trading Flow

┌─────────────────────┐
│  User submits order │
└──────────┬──────────┘

┌───────────────────────────────┐
│  Trade executed               │
│  (OrderBook or RFQ)           │
└──────────────┬────────────────┘

┌────────────────────────────────────────────────┐
│  PositionManager.updatePosition() called       │
│  Position tracked per user + portfolio + series│
└───────────────────────┬────────────────────────┘

┌──────────────────────────────────────────────────┐
│  CollateralVault.checkMargin()                   │
│  Margin checked per portfolio (max 16 positions) │
└──────────────────────────────────────────────────┘

Liquidation Flow

┌──────────────────────────────────┐
│  Liquidation triggered           │
│  (Equity < MM)                   │
└───────────────┬──────────────────┘

┌──────────────────────────────────┐
│  LiquidationEngine identifies    │
│  underwater portfolio            │
└───────────────┬──────────────────┘

┌──────────────────────────────────┐
│  Liquidate positions within      │
│  that portfolio only (max 16)    │
└───────────────┬──────────────────┘

┌──────────────────────────────────┐
│  Other portfolios unaffected     │
└──────────────────────────────────┘

Integration Points

Depends On

ContractPurpose
DiffusalOptionsPositionManagerPosition tracking per portfolio
DiffusalCollateralVaultCollateral per portfolio

Used By

ContractPurpose
DiffusalCollateralVaultCreates portfolios on deposit
DiffusalLiquidationEnginePortfolio-bounded liquidation

Security Considerations

Gas-Bounded Operations

The 16-position limit per portfolio ensures:

  • Liquidation gas costs are bounded and predictable
  • No single liquidation can exceed block gas limit
  • Keepers can reliably estimate liquidation costs

Operator-Only Mutations

All portfolio modifications require operator authorization:

modifier onlyOperator() {
    if (!_getPortfolioManagerStorage().operators[msg.sender]) revert Errors.NotOperator();
    _;
}

Empty Portfolio Deletion

Any empty portfolio can be deleted, including portfolio 0. A portfolio is considered empty when it has no positions and no collateral:

if (positionManager.getPositionCount(user, portfolioId) > 0) revert Errors.PortfolioNotEmpty();
if (vault.getDeposit(user, portfolioId) > 0) revert Errors.PortfolioNotEmpty();

Two-Step Ownership Transfer

Prevents accidental ownership loss through the accept pattern.

Pausable

Emergency pause capability for all portfolio operations.


Code Reference

Source: packages/contracts/src/DiffusalPortfolioManager.sol

Interface: packages/contracts/src/interfaces/IDiffusalPortfolioManager.sol


On this page