Skip to main content

Fee Structure

The Panoptic Protocol charges fees on option minting and premium realization. These fees support protocol sustainability and can be shared with ecosystem builders through a referral system.

Fee Types
​

Commission Fee (Notional Fee)
​

The commission fee is charged when opening new positions, calculated as a percentage of the notional value:

uint128 commission = uint256(int256(shortAmount) + int256(longAmount)).toUint128();
uint128 commissionFee = uint128(
Math.mulDivRoundingUp(commission, riskParameters.notionalFee(), DECIMALS)
);
ParameterDescription
notionalFeeFee rate in basis points (e.g., 10 = 0.1%)
BaseSum of all long and short notional amounts
TimingCharged at position creation (mint)

Example:

  • Opening a position with 1,000 USDC notional
  • Notional fee: 10 bps (0.1%)
  • Commission: 1,000 Γ— 0.001 = 1 USDC

Premium Fee
​

The premium fee is charged when closing positions that have accumulated premium:

uint128 commissionFeeP = uint128(
Math.mulDivRoundingUp(commissionP, riskParameters.premiumFee(), DECIMALS)
);
ParameterDescription
premiumFeeFee rate on realized premium
BaseAbsolute value of realized premium
TimingCharged at position close (burn)
CapLimited to 10Γ— the notional fee equivalent

Fee Cap Logic:

uint128 commissionFeeN = uint128(
Math.mulDivRoundingUp(commissionN, 10 * riskParameters.notionalFee(), DECIMALS)
);
commissionFee = Math.min(commissionFeeP, commissionFeeN).toUint128();

This cap ensures fees remain reasonable even for highly profitable positions.

Fee Distribution
​

Fees can be distributed in two ways depending on whether a builder code is present:

Without Builder Code
​

When no builder code is associated with the transaction, fees are burned (removed from circulation):

if (riskParameters.feeRecipient() == 0) {
_burn(optionOwner, sharesToBurn);
emit CommissionPaid(optionOwner, address(0), commissionFee, 0);
}

Effect: Burning shares increases the value of remaining shares, benefiting all liquidity providers.

With Builder Code
​

When a valid builder code is present, fees are split between the protocol and the builder:

_transferFrom(
optionOwner,
address(riskEngine()),
(sharesToBurn * riskParameters.protocolSplit()) / DECIMALS
);
_transferFrom(
optionOwner,
address(uint160(riskParameters.feeRecipient())),
(sharesToBurn * riskParameters.builderSplit()) / DECIMALS
);
RecipientSplitPurpose
Protocol (RiskEngine)65% (PROTOCOL_SPLIT = 6,500)Protocol treasury
Builder Wallet25% (BUILDER_SPLIT = 2,500)Builder rewards
Burned10% (remainder)LP value accrual

Builder Wallet System

The Builder Wallet system enables ecosystem developers to earn a share of protocol fees by referring users to Panoptic.

Overview
​

Builders are developers, integrators, or partners who build products on top of or integrate with Panoptic. When users interact with the protocol through a builder's interface, the builder receives a portion of the fees generated.

Architecture
​

Builder Factory
​

The BuilderFactory contract deploys Builder Wallets using CREATE2 for deterministic addresses:

contract BuilderFactory {
address public immutable OWNER;

function deployBuilder(
uint48 builderCode,
address builderAdmin
) external onlyOwner returns (address wallet) {
bytes32 salt = bytes32(uint256(builderCode));
bytes memory initCode = abi.encodePacked(
type(BuilderWallet).creationCode,
abi.encode(address(this))
);
wallet = Create2Lib.deploy(0, salt, initCode);
BuilderWallet(wallet).init(builderAdmin);
}
}

Key Features:

  • Only the factory owner can deploy new wallets
  • Each builder code maps to exactly one wallet address
  • Wallet addresses are deterministic and can be computed before deployment

Builder Wallet
​

Each builder receives a dedicated wallet contract:

contract BuilderWallet {
address public immutable FACTORY;
address public builderAdmin;

function sweep(address token, address to) external {
if (msg.sender != builderAdmin) revert Errors.NotBuilder();
uint256 bal = IERC20(token).balanceOf(address(this));
if (bal == 0) return;
IERC20(token).transfer(to, bal);
}
}

Features:

  • Immutable factory reference for security
  • Admin-controlled token sweeping
  • Supports any ERC20 token

Wallet Address Computation
​

The RiskEngine computes builder wallet addresses deterministically:

function _computeBuilderWallet(uint256 builderCode) internal view returns (address wallet) {
if (builderCode == 0) return address(0);

bytes32 salt = bytes32(builderCode);
bytes32 h = keccak256(
abi.encodePacked(
bytes1(0xff),
BUILDER_FACTORY,
salt,
BUILDER_INIT_CODE_HASH
)
);
wallet = address(uint160(uint256(h)));
}

This follows the standard CREATE2 address derivation formula.

Wallet Validation
​

Before distributing fees, the protocol validates the builder wallet:

function getFeeRecipient(uint256 builderCode) external view returns (address feeRecipient) {
feeRecipient = _computeBuilderWallet(builderCode);

// Enforce whitelist by checking contract exists
if (builderCode != 0) {
if (feeRecipient.code.length == 0) revert Errors.InvalidBuilderCode();
}
}

This ensures:

  • Only deployed wallets can receive fees
  • Invalid builder codes revert rather than sending to uncontrolled addresses

Builder Integration Flow
​

1. Builder Registration
​

Builder β†’ Factory Owner: Request builder code
Factory Owner β†’ BuilderFactory: deployBuilder(builderCode, builderAdmin)
BuilderFactory β†’ BuilderWallet: Deploy at deterministic address

2. User Transaction with Builder Code
​

User β†’ Protocol: Transaction with builderCode in calldata
Protocol β†’ RiskEngine: getRiskParameters(tick, oraclePack, builderCode)
RiskEngine: Compute feeRecipient = _computeBuilderWallet(builderCode)
RiskEngine β†’ Protocol: Return RiskParameters with feeRecipient

3. Fee Distribution
​

CollateralTracker: Calculate commission
CollateralTracker β†’ RiskEngine: Transfer protocol share
CollateralTracker β†’ BuilderWallet: Transfer builder share

4. Builder Withdrawal
​

Builder Admin β†’ BuilderWallet: sweep(token, destination)
BuilderWallet β†’ Destination: Transfer accumulated fees

Fee Recipient in RiskParameters
​

The builder wallet address is embedded in the RiskParameters struct:

function getRiskParameters(
int24 currentTick,
OraclePack oraclePack,
uint256 builderCode
) external view returns (RiskParameters) {
uint8 safeMode = isSafeMode(currentTick, oraclePack);
uint128 feeRecipient = uint256(uint160(_computeBuilderWallet(builderCode))).toUint128();

return RiskParametersLibrary.storeRiskParameters(
safeMode,
NOTIONAL_FEE,
PREMIUM_FEE,
PROTOCOL_SPLIT,
BUILDER_SPLIT,
MAX_TWAP_DELTA_LIQUIDATION,
MAX_SPREAD,
BP_DECREASE_BUFFER,
MAX_OPEN_LEGS,
feeRecipient
);
}

Security Considerations
​

Deterministic Addresses
​

Using CREATE2 ensures:

  • Wallet addresses are known before deployment
  • No front-running of wallet creation
  • Consistent addresses across chains (with same factory)

Admin Controls
​

  • Only the designated builderAdmin can withdraw funds
  • Admin is set at initialization and cannot be changed
  • Factory owner controls wallet deployment

Validation
​

  • Builder codes must correspond to deployed wallets
  • Zero builder code results in fee burning (no builder share)
  • Invalid codes cause transaction revert

Summary
​

ComponentPurpose
Notional FeeCommission on position creation
Premium FeeFee on realized premium (capped)
Protocol Split65% of fees to protocol
Builder Split25% of fees to builder
Builder FactoryDeploys deterministic wallets
Builder WalletAccumulates and allows withdrawal of builder fees