mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
big.Int.Uint64 and .Int64 are documented "if x cannot be represented, the result is undefined". On calldata that is not a lossy conversion to reason about, it is a value substitution, and the substitution happens twice in series: Uint64 drops everything above bit 63, and the Go conversion that follows it -- uint24(x), which is uint32(x) because uint24 is an alias -- drops everything above bit 31. A reader who checks only the first concludes the site is safe. contract.Unsigned and contract.Signed refuse a word that does not fit its declared width, once, where the bytes become a value. Six production sites use them; nothing downstream has to remember which of its arguments were bounded. DecodePoolKey read fee and tickSpacing as 24-bit fields and enforced neither. Above 2^32 the narrowing substituted: a tickSpacing word of 2^32+6 arrived as 6, and 2^32+60 produced the same pool id as 60. Between 2^24 and 2^32 the field held the caller's value faithfully while EncodePoolKeyABI wrote only its low three bytes, so Fee 3000 and Fee 2^24+3000 hash to the SAME id -- PoolKey.ID is not injective over its own Go type, and the decoder handed back a key whose fields are not the ones its id was derived from. The encoder is unchanged; changing it renames every pool that exists. What changed is that no such key can be built from calldata, which empties the collision class at the only boundary that produces PoolKeys from untrusted bytes. Same bound on the two tick fields in DecodeModifyLiquidityInput and decodePMLifecycle. Three engines narrowed INSIDE the comparison meant to bound the value, so the check did not merely miss, it inverted. A margin removal to 2^64x leverage read as leverage zero and passed; a perp open at 2^64 in hundredths did the same; increasePosition wrapped the quotient into a uint32 field that then set the liquidation price. Leverage is now compared at full width. CalculateFee clamped AFTER narrowing, so a violent enough move came out cheaper than a calm one; it clamps in 256 bits and narrows once the clamp proves it fits. GetTimeToRepayment saturates rather than wrapping a horizon into a small number of blocks. fhe declares euint128/160/256 and loads them through EncryptUint64, whose bit loop reads value>>i off a uint64 -- zero for every i above 63. The container is genuinely wider (tfheRandom fills all 256 bits), so the missing bits were not rounded away, they were encrypted AS ZERO: measured, asEuint256(2^200) decrypted to 0, and asEaddress on two addresses sharing their last eight bytes produced the same plaintext, so a homomorphic equality over encrypted addresses answered true for two different accounts. Casts to a narrower type still truncate, matching Solidity and the existing asEuint4/8/16/32 masks; what is refused is a plaintext wider than the loader, which is not a cast but a value the encryptor cannot carry. Widening it needs an EncryptBig upstream -- BitCiphertext's fields are unexported and it has no constructor from bits. Same bound on the scalar ops, once ahead of the switch rather than five times inside it. Consensus-visible: words that decoded now refuse. Fee or tickSpacing wider than 24 bits, a tick wider than int24, an FHE plaintext or scalar wider than 64 bits. Every one of them previously resolved to a different value than the caller sent. Four prior lanes had pinned three of these as characterisation tests naming themselves DEFECT, each instructing that the test be inverted when the bound arrived; they are inverted here rather than deleted, so the history of why stays legible. 24 of 24 mutations caught, no survivors. contract 100.0%, dex 99.1 -> 99.2. Both CGO settings, -count=1, -count=2 and -race.
691 lines
18 KiB
Go
691 lines
18 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dex
|
|
|
|
import (
|
|
"math/big"
|
|
"sync"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/zeebo/blake3"
|
|
)
|
|
|
|
// Precompile address (LP-9060 LXLiquid)
|
|
var liquidAddr = common.HexToAddress(LXRepayerAddress)
|
|
|
|
// Storage key prefixes for Liquid state
|
|
var (
|
|
liquidYieldTokenPrefix = []byte("liquid/ytok") // Approved yield tokens
|
|
liquidTokenPrefix = []byte("liquid/syn") // Synthetic tokens
|
|
liquidAccountPrefix = []byte("liquid/acc") // User accounts
|
|
liquidGlobalPrefix = []byte("liquid/glob") // Global state
|
|
)
|
|
|
|
// Liquid implements the self-repaying loan vault precompile
|
|
// Based on Alchemix architecture with 90% LTV (vs 50% in original)
|
|
//
|
|
// Key features:
|
|
// - Deposit yield-bearing tokens (LP tokens) as collateral
|
|
// - Mint liquid tokens (LUSD, LETH) up to 90% of collateral value
|
|
// - Yield automatically harvested and applied to debt repayment
|
|
// - NO LIQUIDATIONS - positions are always solvent (debt <= collateral)
|
|
// - Manual repayment also supported
|
|
type Liquid struct {
|
|
mu sync.RWMutex
|
|
|
|
// Approved yield-bearing tokens that can be used as collateral
|
|
yieldTokens map[common.Address]*YieldToken
|
|
|
|
// Registered liquid tokens that can be minted
|
|
liquidTokens map[common.Address]*LiquidToken
|
|
|
|
// User accounts (keyed by owner + yieldToken)
|
|
accounts map[[32]byte]*LiquidAccount
|
|
|
|
// Reference to pool manager for LP token valuations
|
|
poolManager *PoolManager
|
|
}
|
|
|
|
// NewLiquid creates a new Liquid instance
|
|
func NewLiquid(pm *PoolManager) *Liquid {
|
|
return &Liquid{
|
|
yieldTokens: make(map[common.Address]*YieldToken),
|
|
liquidTokens: make(map[common.Address]*LiquidToken),
|
|
accounts: make(map[[32]byte]*LiquidAccount),
|
|
poolManager: pm,
|
|
}
|
|
}
|
|
|
|
// accountKey generates unique key for user account
|
|
func accountKey(owner common.Address, yieldToken common.Address) [32]byte {
|
|
h := blake3.New()
|
|
h.Write(owner.Bytes())
|
|
h.Write(yieldToken.Bytes())
|
|
var key [32]byte
|
|
h.Digest().Read(key[:])
|
|
return key
|
|
}
|
|
|
|
// =========================================================================
|
|
// Admin Functions (would be controlled by governance)
|
|
// =========================================================================
|
|
|
|
// AddYieldToken registers a new yield-bearing token as valid collateral
|
|
func (a *Liquid) AddYieldToken(
|
|
stateDB StateDB,
|
|
token common.Address,
|
|
underlying Currency,
|
|
yieldPerBlock *big.Int,
|
|
) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
if _, exists := a.yieldTokens[token]; exists {
|
|
return ErrInvalidYieldToken
|
|
}
|
|
|
|
yt := &YieldToken{
|
|
Address: token,
|
|
UnderlyingAsset: underlying,
|
|
YieldPerBlock: new(big.Int).Set(yieldPerBlock),
|
|
IsActive: true,
|
|
TotalDeposited: big.NewInt(0),
|
|
}
|
|
|
|
a.yieldTokens[token] = yt
|
|
a.saveYieldToken(stateDB, yt)
|
|
return nil
|
|
}
|
|
|
|
// AddLiquidToken registers a new liquid token
|
|
func (a *Liquid) AddLiquidToken(
|
|
stateDB StateDB,
|
|
synthetic common.Address,
|
|
underlying Currency,
|
|
debtCeiling *big.Int,
|
|
) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
if _, exists := a.liquidTokens[synthetic]; exists {
|
|
return ErrLiquidTokenNotRegistered
|
|
}
|
|
|
|
st := &LiquidToken{
|
|
Address: synthetic,
|
|
UnderlyingAsset: underlying,
|
|
TotalMinted: big.NewInt(0),
|
|
DebtCeiling: new(big.Int).Set(debtCeiling),
|
|
MintFee: 10, // 0.10%
|
|
BurnFee: 10, // 0.10%
|
|
}
|
|
|
|
a.liquidTokens[synthetic] = st
|
|
a.saveLiquidToken(stateDB, st)
|
|
return nil
|
|
}
|
|
|
|
// =========================================================================
|
|
// Core Liquid Operations
|
|
// =========================================================================
|
|
|
|
// Deposit deposits yield-bearing tokens as collateral
|
|
// This is the first step to taking a self-repaying loan
|
|
func (a *Liquid) Deposit(
|
|
stateDB StateDB,
|
|
owner common.Address,
|
|
yieldToken common.Address,
|
|
amount *big.Int,
|
|
) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
// Verify yield token is approved
|
|
yt, exists := a.yieldTokens[yieldToken]
|
|
if !exists || !yt.IsActive {
|
|
return ErrInvalidYieldToken
|
|
}
|
|
|
|
if amount.Sign() <= 0 {
|
|
return ErrInsufficientCollateral
|
|
}
|
|
|
|
// Get or create account
|
|
key := accountKey(owner, yieldToken)
|
|
account := a.getAccount(stateDB, key)
|
|
if account == nil {
|
|
account = &LiquidAccount{
|
|
Owner: owner,
|
|
YieldToken: yieldToken,
|
|
Collateral: big.NewInt(0),
|
|
Debt: big.NewInt(0),
|
|
LastHarvestBlock: 0,
|
|
AccruedYield: big.NewInt(0),
|
|
}
|
|
}
|
|
|
|
// Harvest any accrued yield first
|
|
a.harvestYieldInternal(stateDB, account, yt)
|
|
|
|
// Transfer yield tokens from user to Liquid
|
|
a.transferFrom(stateDB, yieldToken, owner, liquidAddr, amount)
|
|
|
|
// Update account
|
|
account.Collateral = new(big.Int).Add(account.Collateral, amount)
|
|
|
|
// Update global state
|
|
yt.TotalDeposited = new(big.Int).Add(yt.TotalDeposited, amount)
|
|
|
|
// Save state
|
|
a.saveAccount(stateDB, key, account)
|
|
a.saveYieldToken(stateDB, yt)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Withdraw withdraws yield-bearing collateral
|
|
// Only allowed if it doesn't bring position below minimum collateralization
|
|
func (a *Liquid) Withdraw(
|
|
stateDB StateDB,
|
|
owner common.Address,
|
|
yieldToken common.Address,
|
|
amount *big.Int,
|
|
) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
yt, exists := a.yieldTokens[yieldToken]
|
|
if !exists {
|
|
return ErrInvalidYieldToken
|
|
}
|
|
|
|
key := accountKey(owner, yieldToken)
|
|
account := a.getAccount(stateDB, key)
|
|
if account == nil || account.Collateral.Sign() == 0 {
|
|
return ErrInsufficientCollateral
|
|
}
|
|
|
|
// Harvest yield first
|
|
a.harvestYieldInternal(stateDB, account, yt)
|
|
|
|
// Check withdrawal doesn't exceed collateral
|
|
if amount.Cmp(account.Collateral) > 0 {
|
|
return ErrInsufficientCollateral
|
|
}
|
|
|
|
// Calculate new collateral after withdrawal
|
|
newCollateral := new(big.Int).Sub(account.Collateral, amount)
|
|
|
|
// Ensure position remains healthy (debt <= 90% of collateral)
|
|
// Since this is Alchemix-style, debt can never exceed collateral
|
|
if account.Debt.Sign() > 0 {
|
|
maxDebt := a.calculateMaxDebt(newCollateral)
|
|
if account.Debt.Cmp(maxDebt) > 0 {
|
|
return ErrMaxLTVExceeded
|
|
}
|
|
}
|
|
|
|
// Update account
|
|
account.Collateral = newCollateral
|
|
|
|
// Update global state
|
|
yt.TotalDeposited = new(big.Int).Sub(yt.TotalDeposited, amount)
|
|
|
|
// Transfer yield tokens back to user
|
|
a.transfer(stateDB, yieldToken, liquidAddr, owner, amount)
|
|
|
|
// Save state
|
|
a.saveAccount(stateDB, key, account)
|
|
a.saveYieldToken(stateDB, yt)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Mint mints liquid tokens against deposited collateral
|
|
// Maximum 90% LTV (vs Alchemix's 50%)
|
|
func (a *Liquid) Mint(
|
|
stateDB StateDB,
|
|
owner common.Address,
|
|
yieldToken common.Address,
|
|
syntheticToken common.Address,
|
|
amount *big.Int,
|
|
) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
// Verify yield token and liquid token
|
|
yt, exists := a.yieldTokens[yieldToken]
|
|
if !exists || !yt.IsActive {
|
|
return ErrInvalidYieldToken
|
|
}
|
|
|
|
st, exists := a.liquidTokens[syntheticToken]
|
|
if !exists {
|
|
return ErrLiquidTokenNotRegistered
|
|
}
|
|
|
|
// Check debt ceiling
|
|
newTotalMinted := new(big.Int).Add(st.TotalMinted, amount)
|
|
if newTotalMinted.Cmp(st.DebtCeiling) > 0 {
|
|
return ErrDebtCeiling
|
|
}
|
|
|
|
// Get account
|
|
key := accountKey(owner, yieldToken)
|
|
account := a.getAccount(stateDB, key)
|
|
if account == nil || account.Collateral.Sign() == 0 {
|
|
return ErrInsufficientCollateral
|
|
}
|
|
|
|
// Harvest yield first
|
|
a.harvestYieldInternal(stateDB, account, yt)
|
|
|
|
// Calculate max mintable (90% of collateral value minus existing debt)
|
|
collateralValue := a.getCollateralValue(stateDB, account.Collateral, yt)
|
|
maxDebt := a.calculateMaxDebt(collateralValue)
|
|
availableToMint := new(big.Int).Sub(maxDebt, account.Debt)
|
|
|
|
if amount.Cmp(availableToMint) > 0 {
|
|
return ErrMaxLTVExceeded
|
|
}
|
|
|
|
// Apply mint fee
|
|
feeAmount := a.calculateFee(amount, st.MintFee)
|
|
netMintAmount := new(big.Int).Sub(amount, feeAmount)
|
|
|
|
// Update account debt
|
|
account.Debt = new(big.Int).Add(account.Debt, amount)
|
|
|
|
// Update synthetic total minted
|
|
st.TotalMinted = newTotalMinted
|
|
|
|
// Mint liquid tokens to user
|
|
a.mintSynthetic(stateDB, syntheticToken, owner, netMintAmount)
|
|
|
|
// Save state
|
|
a.saveAccount(stateDB, key, account)
|
|
a.saveLiquidToken(stateDB, st)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Burn burns liquid tokens to reduce debt
|
|
// Manual repayment option
|
|
func (a *Liquid) Burn(
|
|
stateDB StateDB,
|
|
owner common.Address,
|
|
yieldToken common.Address,
|
|
syntheticToken common.Address,
|
|
amount *big.Int,
|
|
) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
st, exists := a.liquidTokens[syntheticToken]
|
|
if !exists {
|
|
return ErrLiquidTokenNotRegistered
|
|
}
|
|
|
|
key := accountKey(owner, yieldToken)
|
|
account := a.getAccount(stateDB, key)
|
|
if account == nil || account.Debt.Sign() == 0 {
|
|
return ErrNoDebtToRepay
|
|
}
|
|
|
|
yt := a.yieldTokens[yieldToken]
|
|
if yt != nil {
|
|
a.harvestYieldInternal(stateDB, account, yt)
|
|
}
|
|
|
|
// Cap burn amount to outstanding debt
|
|
burnAmount := amount
|
|
if burnAmount.Cmp(account.Debt) > 0 {
|
|
burnAmount = new(big.Int).Set(account.Debt)
|
|
}
|
|
|
|
// Apply burn fee
|
|
feeAmount := a.calculateFee(burnAmount, st.BurnFee)
|
|
debtReduction := new(big.Int).Sub(burnAmount, feeAmount)
|
|
|
|
// Burn liquid tokens from user
|
|
a.burnSynthetic(stateDB, syntheticToken, owner, burnAmount)
|
|
|
|
// Reduce debt
|
|
account.Debt = new(big.Int).Sub(account.Debt, debtReduction)
|
|
if account.Debt.Sign() < 0 {
|
|
account.Debt = big.NewInt(0)
|
|
}
|
|
|
|
// Update synthetic total
|
|
st.TotalMinted = new(big.Int).Sub(st.TotalMinted, burnAmount)
|
|
|
|
// Save state
|
|
a.saveAccount(stateDB, key, account)
|
|
a.saveLiquidToken(stateDB, st)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Harvest harvests accrued yield and applies it to debt repayment
|
|
// This is the "self-repaying" mechanism
|
|
func (a *Liquid) Harvest(
|
|
stateDB StateDB,
|
|
owner common.Address,
|
|
yieldToken common.Address,
|
|
) (*big.Int, error) {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
yt, exists := a.yieldTokens[yieldToken]
|
|
if !exists {
|
|
return nil, ErrInvalidYieldToken
|
|
}
|
|
|
|
key := accountKey(owner, yieldToken)
|
|
account := a.getAccount(stateDB, key)
|
|
if account == nil {
|
|
return nil, ErrInsufficientCollateral
|
|
}
|
|
|
|
harvested := a.harvestYieldInternal(stateDB, account, yt)
|
|
|
|
a.saveAccount(stateDB, key, account)
|
|
|
|
return harvested, nil
|
|
}
|
|
|
|
// =========================================================================
|
|
// Internal Yield Harvesting
|
|
// =========================================================================
|
|
|
|
// harvestYieldInternal calculates and applies accrued yield to debt
|
|
func (a *Liquid) harvestYieldInternal(
|
|
stateDB StateDB,
|
|
account *LiquidAccount,
|
|
yt *YieldToken,
|
|
) *big.Int {
|
|
// Get current block
|
|
currentBlock := a.getCurrentBlock(stateDB)
|
|
|
|
if account.LastHarvestBlock == 0 {
|
|
account.LastHarvestBlock = currentBlock
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
// Calculate blocks elapsed
|
|
blocksElapsed := currentBlock - account.LastHarvestBlock
|
|
if blocksElapsed == 0 {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
// Calculate yield: collateral * yieldPerBlock * blocksElapsed / 1e18
|
|
yieldAmount := new(big.Int).Mul(account.Collateral, yt.YieldPerBlock)
|
|
yieldAmount.Mul(yieldAmount, big.NewInt(int64(blocksElapsed)))
|
|
yieldAmount.Div(yieldAmount, big.NewInt(1e18))
|
|
|
|
// Add to accrued yield
|
|
account.AccruedYield = new(big.Int).Add(account.AccruedYield, yieldAmount)
|
|
|
|
// Apply accrued yield to debt repayment
|
|
if account.Debt.Sign() > 0 && account.AccruedYield.Sign() > 0 {
|
|
if account.AccruedYield.Cmp(account.Debt) >= 0 {
|
|
// Yield covers all debt
|
|
account.AccruedYield = new(big.Int).Sub(account.AccruedYield, account.Debt)
|
|
account.Debt = big.NewInt(0)
|
|
} else {
|
|
// Partial debt repayment
|
|
account.Debt = new(big.Int).Sub(account.Debt, account.AccruedYield)
|
|
account.AccruedYield = big.NewInt(0)
|
|
}
|
|
}
|
|
|
|
account.LastHarvestBlock = currentBlock
|
|
|
|
return yieldAmount
|
|
}
|
|
|
|
// =========================================================================
|
|
// View Functions
|
|
// =========================================================================
|
|
|
|
// GetAccount returns a user's account state
|
|
func (a *Liquid) GetAccount(
|
|
stateDB StateDB,
|
|
owner common.Address,
|
|
yieldToken common.Address,
|
|
) *LiquidAccount {
|
|
a.mu.RLock()
|
|
defer a.mu.RUnlock()
|
|
|
|
key := accountKey(owner, yieldToken)
|
|
return a.getAccount(stateDB, key)
|
|
}
|
|
|
|
// GetMaxMintable returns the maximum amount a user can mint
|
|
func (a *Liquid) GetMaxMintable(
|
|
stateDB StateDB,
|
|
owner common.Address,
|
|
yieldToken common.Address,
|
|
) *big.Int {
|
|
a.mu.RLock()
|
|
defer a.mu.RUnlock()
|
|
|
|
yt, exists := a.yieldTokens[yieldToken]
|
|
if !exists {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
key := accountKey(owner, yieldToken)
|
|
account := a.getAccount(stateDB, key)
|
|
if account == nil || account.Collateral.Sign() == 0 {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
collateralValue := a.getCollateralValue(stateDB, account.Collateral, yt)
|
|
maxDebt := a.calculateMaxDebt(collateralValue)
|
|
|
|
// Account for accrued yield that will reduce debt
|
|
effectiveDebt := new(big.Int).Sub(account.Debt, account.AccruedYield)
|
|
if effectiveDebt.Sign() < 0 {
|
|
effectiveDebt = big.NewInt(0)
|
|
}
|
|
|
|
available := new(big.Int).Sub(maxDebt, effectiveDebt)
|
|
if available.Sign() < 0 {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
return available
|
|
}
|
|
|
|
// GetLTV returns the current loan-to-value ratio for an account
|
|
func (a *Liquid) GetLTV(
|
|
stateDB StateDB,
|
|
owner common.Address,
|
|
yieldToken common.Address,
|
|
) *big.Int {
|
|
a.mu.RLock()
|
|
defer a.mu.RUnlock()
|
|
|
|
yt, exists := a.yieldTokens[yieldToken]
|
|
if !exists {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
key := accountKey(owner, yieldToken)
|
|
account := a.getAccount(stateDB, key)
|
|
if account == nil || account.Collateral.Sign() == 0 {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
collateralValue := a.getCollateralValue(stateDB, account.Collateral, yt)
|
|
if collateralValue.Sign() == 0 {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
// LTV = (debt * 10000) / collateralValue
|
|
ltv := new(big.Int).Mul(account.Debt, big.NewInt(LTVPrecision))
|
|
ltv.Div(ltv, collateralValue)
|
|
|
|
return ltv
|
|
}
|
|
|
|
// GetTimeToRepayment estimates blocks until debt is fully repaid
|
|
func (a *Liquid) GetTimeToRepayment(
|
|
stateDB StateDB,
|
|
owner common.Address,
|
|
yieldToken common.Address,
|
|
) uint64 {
|
|
a.mu.RLock()
|
|
defer a.mu.RUnlock()
|
|
|
|
yt, exists := a.yieldTokens[yieldToken]
|
|
if !exists || yt.YieldPerBlock.Sign() == 0 {
|
|
return 0
|
|
}
|
|
|
|
key := accountKey(owner, yieldToken)
|
|
account := a.getAccount(stateDB, key)
|
|
if account == nil || account.Debt.Sign() == 0 {
|
|
return 0
|
|
}
|
|
|
|
// Calculate yield per block for this position
|
|
yieldPerBlock := new(big.Int).Mul(account.Collateral, yt.YieldPerBlock)
|
|
yieldPerBlock.Div(yieldPerBlock, big.NewInt(1e18))
|
|
|
|
if yieldPerBlock.Sign() == 0 {
|
|
return 0 // Would never repay
|
|
}
|
|
|
|
// Blocks = debt / yieldPerBlock
|
|
effectiveDebt := new(big.Int).Sub(account.Debt, account.AccruedYield)
|
|
if effectiveDebt.Sign() <= 0 {
|
|
return 0 // Already repaid
|
|
}
|
|
|
|
blocks := new(big.Int).Div(effectiveDebt, yieldPerBlock)
|
|
// Saturate rather than wrap. A horizon too far away to name is still far
|
|
// away; its low 64 bits are an arbitrary small number, and this value is
|
|
// read as "how long until this debt clears".
|
|
if !blocks.IsUint64() {
|
|
return ^uint64(0)
|
|
}
|
|
return blocks.Uint64()
|
|
}
|
|
|
|
// =========================================================================
|
|
// Helper Functions
|
|
// =========================================================================
|
|
|
|
// calculateMaxDebt calculates maximum debt for given collateral (90% LTV)
|
|
func (a *Liquid) calculateMaxDebt(collateralValue *big.Int) *big.Int {
|
|
// maxDebt = collateralValue * MaxLTV / LTVPrecision
|
|
// MaxLTV = 9000 (90%)
|
|
maxDebt := new(big.Int).Mul(collateralValue, big.NewInt(MaxLTV))
|
|
maxDebt.Div(maxDebt, big.NewInt(LTVPrecision))
|
|
return maxDebt
|
|
}
|
|
|
|
// calculateFee calculates fee amount
|
|
func (a *Liquid) calculateFee(amount *big.Int, feeBps uint24) *big.Int {
|
|
fee := new(big.Int).Mul(amount, big.NewInt(int64(feeBps)))
|
|
fee.Div(fee, big.NewInt(1_000_000)) // Fee in basis points (1 bp = 0.01%)
|
|
return fee
|
|
}
|
|
|
|
// getCollateralValue returns the value of collateral in underlying terms
|
|
func (a *Liquid) getCollateralValue(stateDB StateDB, amount *big.Int, yt *YieldToken) *big.Int {
|
|
// For LP tokens, would query the pool for underlying value
|
|
// Simplified: assume 1:1 for now
|
|
// In production, this would call poolManager to get real value
|
|
return new(big.Int).Set(amount)
|
|
}
|
|
|
|
// getCurrentBlock returns the current block number
|
|
func (a *Liquid) getCurrentBlock(stateDB StateDB) uint64 {
|
|
// In production, would read from block context
|
|
// For testing, use state-based tracking
|
|
blockKey := makeStorageKey(liquidGlobalPrefix, []byte("block"))
|
|
blockHash := stateDB.GetState(liquidAddr, blockKey)
|
|
if blockHash == (common.Hash{}) {
|
|
return 1
|
|
}
|
|
return uint256.NewInt(0).SetBytes(blockHash[:]).Uint64()
|
|
}
|
|
|
|
// =========================================================================
|
|
// Storage Management
|
|
// =========================================================================
|
|
|
|
func (a *Liquid) getAccount(stateDB StateDB, key [32]byte) *LiquidAccount {
|
|
if acc, ok := a.accounts[key]; ok {
|
|
return acc
|
|
}
|
|
|
|
// Load from state
|
|
storageKey := makeStorageKey(liquidAccountPrefix, key[:])
|
|
data := stateDB.GetState(liquidAddr, storageKey)
|
|
if data == (common.Hash{}) {
|
|
return nil
|
|
}
|
|
|
|
// Deserialize (simplified - in production would be full encoding)
|
|
acc := &LiquidAccount{
|
|
Collateral: big.NewInt(0).SetBytes(data[:16]),
|
|
Debt: big.NewInt(0).SetBytes(data[16:]),
|
|
AccruedYield: big.NewInt(0),
|
|
}
|
|
a.accounts[key] = acc
|
|
return acc
|
|
}
|
|
|
|
func (a *Liquid) saveAccount(stateDB StateDB, key [32]byte, acc *LiquidAccount) {
|
|
a.accounts[key] = acc
|
|
|
|
// Save to state (simplified)
|
|
storageKey := makeStorageKey(liquidAccountPrefix, key[:])
|
|
var data common.Hash
|
|
collateralBytes := acc.Collateral.Bytes()
|
|
debtBytes := acc.Debt.Bytes()
|
|
copy(data[:16], collateralBytes)
|
|
copy(data[16:], debtBytes)
|
|
stateDB.SetState(liquidAddr, storageKey, data)
|
|
}
|
|
|
|
func (a *Liquid) saveYieldToken(stateDB StateDB, yt *YieldToken) {
|
|
a.yieldTokens[yt.Address] = yt
|
|
// In production, would serialize full struct to state
|
|
}
|
|
|
|
func (a *Liquid) saveLiquidToken(stateDB StateDB, st *LiquidToken) {
|
|
a.liquidTokens[st.Address] = st
|
|
// In production, would serialize full struct to state
|
|
}
|
|
|
|
// Token transfer helpers (simplified)
|
|
func (a *Liquid) transfer(stateDB StateDB, token common.Address, from, to common.Address, amount *big.Int) {
|
|
// In production, would call ERC20 transfer
|
|
amountU256, _ := uint256.FromBig(amount)
|
|
stateDB.SubBalance(from, amountU256)
|
|
stateDB.AddBalance(to, amountU256)
|
|
}
|
|
|
|
func (a *Liquid) transferFrom(stateDB StateDB, token common.Address, from, to common.Address, amount *big.Int) {
|
|
a.transfer(stateDB, token, from, to, amount)
|
|
}
|
|
|
|
func (a *Liquid) mintSynthetic(stateDB StateDB, token common.Address, to common.Address, amount *big.Int) {
|
|
// In production, would call liquid token mint
|
|
amountU256, _ := uint256.FromBig(amount)
|
|
stateDB.AddBalance(to, amountU256)
|
|
}
|
|
|
|
func (a *Liquid) burnSynthetic(stateDB StateDB, token common.Address, from common.Address, amount *big.Int) {
|
|
// In production, would call liquid token burn
|
|
amountU256, _ := uint256.FromBig(amount)
|
|
stateDB.SubBalance(from, amountU256)
|
|
}
|