Protocol Architecture
Osito Protocol consists of a set of immutable, non-upgradable smart contracts that work together to implement a mathematical lending model. This document explains how the protocol's architecture calculates safe lending limits based on AMM pool state.
Core Design Philosophy
The architecture is built around a mathematical approach to lending limits:
Instead of relying on price oracles, the protocol calculates safe lending limits based on AMM pool state.
This approach allows the protocol to determine lending limits using only on-chain data, without needing external price feeds. The core formula is:
max_borrow = pool_BERA - extractable_BERA
Where extractable_BERA
represents the maximum BERA that could be extracted by selling all circulating tokens into the pool.
Contract Overview
The protocol consists of three core contracts:
- OsitoToken: Verifies token eligibility and provides pool data for borrow limit calculations
- OsitoLending: Implements the core lending, borrowing, and staking functionality
- OsitoFactory: Deploys and links the other contracts (one-time deployment)
Supporting libraries:
- OsitoBorrowLimit: Implements the mathematical model for calculating borrow limits
- OsitoRateCurve: Implements the standard two-slope interest rate model
Contract Relationships
┌───────────────────┐
│ OsitoFactory │
│ (Deployer Only) │
└─────────┬─────────┘
│ deploys
▼
┌───────────────────┐ references ┌───────────────────┐
│ OsitoToken │◄─────────────────────►│ OsitoLending │
│ (Eligibility/Data) │ │ (Core Protocol) │
└───────────────────┘ └────────┬──────────┘
│
▼
┌───────────────────┐
│ AMM Pool Data │
│ (Borrow Calc) │
└───────────────────┘
OsitoToken Contract: Token Verification
The OsitoToken contract serves two key functions:
- Verify token eligibility to ensure the mathematical model can be applied
- Provide pool data for borrow limit calculations
Eligibility Verification
The contract verifies two objective criteria:
function isEligibleToken(address token) external view override returns (bool) {
// 1. Check fixed supply (deployed by whitelisted factory)
if (!isWhitelistedDeployer(token)) {
return false;
}
// 2. Check valid liquidity pool
address pair = getTokenWberaPair(token);
if (pair == address(0)) {
return false;
}
// 3. Check burned LP (prevents direct liquidity withdrawal)
uint256 burnedLP = IERC20(pair).balanceOf(BURN_ADDRESS);
if (burnedLP < MINIMUM_BURNED_LP) {
return false;
}
return true;
}
Pool Data For Borrow Calculations
The contract provides data needed for the borrow limit formula:
function getTokensInPool(address token) external view override returns (uint256) {
address pair = getTokenWberaPair(token);
if (pair == address(0)) return 0;
// Get reserves from the pair
(uint112 reserve0, uint112 reserve1, ) = IKodiakPair(pair).getReserves();
// Determine which reserve is the token
address token0 = IKodiakPair(pair).token0();
return token0 == token ? uint256(reserve0) : uint256(reserve1);
}
function getWberaInPool(address token) external view override returns (uint256) {
address pair = getTokenWberaPair(token);
if (pair == address(0)) return 0;
// Get reserves from the pair
(uint112 reserve0, uint112 reserve1, ) = IKodiakPair(pair).getReserves();
// Determine which reserve is wBERA
address token0 = IKodiakPair(pair).token0();
return token0 == wbera ? uint256(reserve0) : uint256(reserve1);
}
function getTokenTotalSupply(address token) external view override returns (uint256) {
return IERC20(token).totalSupply();
}
OsitoBorrowLimit Library: Mathematical Model
This library implements the core borrow limit calculation:
function calculateMaxBorrow(
address token,
IOsitoToken tokenVerifier,
uint256 tokensDeposited,
uint256 tokensStaked
) internal view returns (uint256) {
// Get token information from the verifier
uint256 totalSupply = tokenVerifier.getTokenTotalSupply(token);
uint256 tokensInPool = tokenVerifier.getTokensInPool(token);
uint256 wberaInPool = tokenVerifier.getWberaInPool(token);
// Early safety checks
if (wberaInPool == 0 || tokensInPool == 0) {
return 0;
}
// Calculate tokens controlled by protocol (deposited + staked)
uint256 protocolControlledTokens = tokensDeposited + tokensStaked;
// Calculate dumpable tokens
uint256 dumpableTokens;
if (totalSupply > (tokensInPool + protocolControlledTokens)) {
unchecked {
dumpableTokens = totalSupply - (tokensInPool + protocolControlledTokens);
}
} else {
dumpableTokens = 0;
}
// If no dumpable tokens, max borrow is the full wBERA in pool
if (dumpableTokens == 0) {
return wberaInPool;
}
// Calculate with improved precision
uint256 numerator = wberaInPool * tokensInPool * PRECISION;
uint256 denominator = (tokensInPool + dumpableTokens) * PRECISION;
uint256 wberaAfterDump = numerator / denominator;
// Maximum borrow is wBERA in pool minus the impact of a token dump
return wberaInPool > wberaAfterDump ? wberaInPool - wberaAfterDump : 0;
}
OsitoLending Contract: Core Protocol
This contract implements the main protocol functionality:
State Variables
// Protocol state
uint256 public totalWberaSupplied;
uint256 public totalWberaBorrowed;
uint256 public wberaRate;
uint256 public wberaYieldRate;
uint256 public lastWberaRateUpdate;
// Position tracking
uint256 public nextPositionId = 1;
mapping(uint256 => Position) public positions;
// Token tracking
mapping(address => TokenData) public tokenData;
mapping(address => mapping(address => uint256)) public stakedAmount;
Key Functions
The contract enforces borrow limits in all operations:
function borrow(uint256 positionId, uint256 amount) external nonReentrant {
// Update states with fresh data
_updateWberaRate();
_updateTokenState(token);
_updatePosition(positionId);
// Calculate position's max borrow
uint256 maxBorrow = calculatePositionMaxBorrow(positionId);
if (position.borrowedAmount + amount > maxBorrow) {
revert("OsitoLending: Borrow amount exceeds limit");
}
// ... rest of function
}
Liquidation Mechanism
Liquidation ensures protocol solvency:
function liquidate(uint256 positionId) external nonReentrant {
// Update state with fresh data
_updateWberaRate();
_updateTokenState(token);
_updatePosition(positionId);
// Check if position is liquidatable
uint256 maxBorrow = calculatePositionMaxBorrow(positionId);
if (position.borrowedAmount <= maxBorrow) {
revert("OsitoLending: Position not liquidatable");
}
// ... liquidation logic
// Close the position
delete positions[positionId];
}
Key Protocol Patterns
The architecture implements several important patterns:
1. Real-Time State Updates
The protocol updates state at key operations to ensure calculations use fresh data:
- Updates wBERA rate
- Updates token state
- Updates position state
2. Precision Management
The protocol uses precision constants (1e18) to maintain accuracy in calculations:
uint256 private constant PRECISION = 1e18;
3. Safety Checks
Key safety checks are implemented throughout:
- Zero amount checks
- Token eligibility verification
- Borrow limit enforcement
- Liquidation conditions
4. Gas Optimization
The protocol includes gas optimizations:
- Unchecked blocks where safe
- Efficient storage patterns
- Minimal state updates
Protocol Benefits
This architecture provides several benefits:
- No Oracle Dependence: Uses on-chain AMM data instead of price feeds
- Objective Token Criteria: Token eligibility based on verifiable properties
- Mathematical Borrow Limits: Lending limits based on pool state
- Efficient Implementation: Gas-optimized with minimal state updates