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)
);
| Parameter | Description |
|---|---|
notionalFee | Fee rate in basis points (e.g., 10 = 0.1%) |
| Base | Sum of all long and short notional amounts |
| Timing | Charged 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)
);
| Parameter | Description |
|---|---|
premiumFee | Fee rate on realized premium |
| Base | Absolute value of realized premium |
| Timing | Charged at position close (burn) |
| Cap | Limited 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
);
| Recipient | Split | Purpose |
|---|---|---|
| Protocol (RiskEngine) | 65% (PROTOCOL_SPLIT = 6,500) | Protocol treasury |
| Builder Wallet | 25% (BUILDER_SPLIT = 2,500) | Builder rewards |
| Burned | 10% (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
builderAdmincan 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β
| Component | Purpose |
|---|---|
| Notional Fee | Commission on position creation |
| Premium Fee | Fee on realized premium (capped) |
| Protocol Split | 65% of fees to protocol |
| Builder Split | 25% of fees to builder |
| Builder Factory | Deploys deterministic wallets |
| Builder Wallet | Accumulates and allows withdrawal of builder fees |