mirror of
https://github.com/luxfi/bank.git
synced 2026-10-30 17:02:07 +00:00
Every money bug this session was int64 meeting int64. A debt in cents was subtracted from a balance in an asset's own units and a +$340 vault position rendered as -$5,600; a repayment sized in the collateral's decimals reached a contract counting the synthetic's, off by 1e10. The comments carried the unit — CollateralUsd int64 // USD cents — and comments do not typecheck. Cents and Minor are distinct types. The only crossing is usd(), which needs a price, which is why it should be the only one. The compiler found every place they met, including the control in the Earn guard, which deliberately reproduces the old sum: it no longer compiles, so it now casts by hand and says why. money[T] reads a whole amount off a record and rounds, once, instead of at the thirty call sites that each had to remember — one of which had not. An existing database could not boot. Base refuses to update any collection whose name equals its own id, because checkUniqueName looks up that id and does not exclude the collection being saved. Every collection here is NewBaseCollection(name, name), so each was creatable once and never updatable, and a schema addition failed a running bank at startup. The upgrade path saves without that validation; nothing there changes a name. Reproduced on a database written by the older build and now booting clean on this one. The dash also ships as a library. lux.finance renders it whole; the Lux Cloud console renders the same screens beside a validator's nodes and keys. bankd validates a bearer at IAM's userinfo without checking audience, so the console's Lux ID session already authorizes the bank — no second sign-in, no exchange. The grid compiler runs at library build time, so a host needs no plugin of its own. Colour belonged to the brand tokens but the layout runtime painted body from its own theme, unlayered, so everything that inherited landed on the wrong brand — white text on the light brand's white page, across the whole app. Co-authored-by: Hanzo Dev <[email protected]>
94 lines
4.2 KiB
Go
94 lines
4.2 KiB
Go
package collections
|
|
|
|
import "github.com/hanzoai/base/core"
|
|
|
|
// Vault is a Liquid Protocol market: deposit a yield-bearing collateral asset,
|
|
// borrow the vault's synthetic x-token against it up to MaxLTV, and the
|
|
// collateral's yield (APY) flows to repay the debt — a self-repaying loan.
|
|
// This is a curated catalog (like Plans), not user data. Underlying is the
|
|
// balance currency actually moved, in and out and on both sides of the loan:
|
|
// the collateral is that asset in yield-bearing form and the synthetic tracks
|
|
// it at parity, so one vault deals in one asset.
|
|
type Vault struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Collateral string `json:"collateral"` // yield-bearing form deposited (stLUX, wstETH…)
|
|
Underlying string `json:"underlying"` // balance currency moved (LUX, ETH, USD, DAI)
|
|
Synthetic string `json:"synthetic"` // x-token borrowed against it (xLUX, xETH, xUSD)
|
|
APY float64 `json:"apy"` // collateral yield, percent per year
|
|
MaxLTV float64 `json:"maxLtv"` // borrow ceiling as a fraction of the collateral itself
|
|
TVLUsd int64 `json:"tvlUsd"` // total value locked, USD cents
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// Vaults is the published Liquid Protocol market list.
|
|
var Vaults = []Vault{
|
|
{
|
|
ID: "stlux", Name: "Staked LUX", Collateral: "stLUX", Underlying: "LUX", Synthetic: "xLUX",
|
|
APY: 8.2, MaxLTV: 0.90, TVLUsd: 48_200_000_00,
|
|
Description: "Stake LUX for network yield and borrow xLUX against it. Staking rewards repay the loan.",
|
|
},
|
|
{
|
|
ID: "wsteth", Name: "Wrapped stETH", Collateral: "wstETH", Underlying: "ETH", Synthetic: "xETH",
|
|
APY: 3.4, MaxLTV: 0.90, TVLUsd: 126_500_000_00,
|
|
Description: "Deposit ETH as wstETH and borrow xETH. Ethereum staking yield flows to your debt.",
|
|
},
|
|
{
|
|
ID: "usdc", Name: "USD Coin", Collateral: "USDC", Underlying: "USD", Synthetic: "xUSD",
|
|
APY: 6.5, MaxLTV: 0.90, TVLUsd: 89_400_000_00,
|
|
Description: "Deposit USD into money-market strategies and borrow stable xUSD against it.",
|
|
},
|
|
{
|
|
ID: "sdai", Name: "Savings DAI", Collateral: "sDAI", Underlying: "DAI", Synthetic: "xUSD",
|
|
APY: 5.1, MaxLTV: 0.85, TVLUsd: 31_800_000_00,
|
|
Description: "Earn the DAI savings rate and borrow xUSD without selling your DAI.",
|
|
},
|
|
}
|
|
|
|
const PositionCollectionName = "positions"
|
|
|
|
// EnsurePositionCollection creates the positions collection: one record per
|
|
// account per vault, holding deposited collateral and outstanding debt, both in
|
|
// minor units of the vault's underlying. Superuser-only; read/written through
|
|
// the authenticated /v1/bank/earn routes.
|
|
func EnsurePositionCollection(app core.App) error {
|
|
if existing, err := app.FindCollectionByNameOrId(PositionCollectionName); err == nil {
|
|
if existing.Fields.GetByName("tokenId") == nil {
|
|
existing.Fields.Add(tokenIDField())
|
|
return app.Save(existing)
|
|
}
|
|
return nil
|
|
}
|
|
c := core.NewBaseCollection(PositionCollectionName, PositionCollectionName)
|
|
c.Fields.Add(
|
|
&core.RelationField{
|
|
Name: "account",
|
|
CollectionId: AccountCollectionName,
|
|
Required: true,
|
|
MaxSelect: 1,
|
|
},
|
|
// Vault catalog id (stlux, wsteth, …).
|
|
&core.TextField{Name: "vault", Required: true},
|
|
// Deposited collateral, in minor units of the vault's underlying asset.
|
|
// Not Required: a position legitimately holds 0 on one side (a fresh
|
|
// deposit before borrowing, or a fully repaid loan), and Base treats a
|
|
// required number's 0 as blank — which would 500 the save.
|
|
&core.NumberField{Name: "collateral"},
|
|
// Outstanding borrowed debt, in the same units as the collateral above:
|
|
// the market lends that asset's own synthetic at parity, so the two sides
|
|
// are like-kind and their ratio is the loan-to-value with no price in it.
|
|
&core.NumberField{Name: "debt"},
|
|
tokenIDField(),
|
|
&core.AutodateField{Name: "created", OnCreate: true},
|
|
&core.AutodateField{Name: "updated", OnCreate: true, OnUpdate: true},
|
|
)
|
|
return app.Save(c)
|
|
}
|
|
|
|
// tokenIDField holds the position NFT that carries this loan on chain, and zero
|
|
// when the position is only a ledger row — which of the two it is says where the
|
|
// loan lives, not what the numbers beside it mean.
|
|
func tokenIDField() *core.NumberField {
|
|
return &core.NumberField{Name: "tokenId", OnlyInt: true}
|
|
}
|