owned this note
owned this note
Published
Linked with GitHub
# Margin Trading Module Implementation
This companion document is an implementation guide to the margin trading system in the [Preliminary Math Specification](https://hackmd.io/maG-kkCeRbKOcup5gfxSWg) and [Mechanism Specification](https://hackmd.io/e2K_VzC-TFyC8MDjqHwczA).
### Notes:
A few mechanisms and experiments from the Margin Trading Model list do not have counterparts in this implementation guide. Links are to the merged notebooks:
- [excess_liabilities](https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/jesse/excess_liabilities.ipynb)
- [force_close_rewards](https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/jesse/force_close_rewards.ipynb)
- [force_close_any_address](https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/jesse/force_close_address.ipynb)
- [global_settlement](https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/jesse/force_close_rewards.ipynb)
-
## State
### Liquidity Pool Global State
State necessary to maintain legal state of the liquidity module with margin trading enabled.
| Name | Schema |
| ---- | ------ |
| Pool | Struct |
| Variable Name | Variable Type | Descriptive Name | Description | Symbol |
| ------------------- | ------------- | ------------------------------ | ---------------------------------------------------- | -------------- |
| ExternalAssetBalance | sdk.Uint | External Assets | Quantity of external tokens as an asset | $X_{A}$ |
| ExternalLiabilities | sdk.Uint | External Principal Liabilities | Quantity of external tokens as a principal liability | $X^P_{L}$ |
| ExternalCustody | sdk.Uint | External Custody | Quantity of external tokens held in custody | $X_{C}$ |
| NativeAssetBalance | sdk.Uint | Native Assets | Quantity of native tokens as an asset | $Y_{A}$ |
| NativeLiabilities | sdk.Uint | Native Principal Liabilities | Quantity of native tokens as a liability | $Y^P_{L}$ |
| NativeCustody | sdk.Uint | Native Custody | Quantity of native tokens held in custody | $Y_{C}$ |
| Health | sdk.Dec | Pool Health | Metric describing the health of an individual Pool | $H(\mathbf X)$ |
| InterestRate | sdk.Dec | Effective Interest Rate for the current epoch | Interest Rate for the current epoch, respecting the limits in both value and differential movement | $\beta$ |
| MarginEnabled | bool | Margin Enabled Parameter | Param for enabling margin on individual pool |
### Local MTP State
###### tags: `Ready for Review`
Local state is the state necessary for having a legal and active account with a margin trading position ($mtp$).
| Name | Schema |
| ---- | ------ |
| MTP | Struct |
| Variable Name | Variable Type | Descriptive Name | Description | Symbol |
| ------------------- | -------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------- |
| Address | sdk.AccAddress | Agent address |
| CollateralAmount | sdk.Uint | Size of action (external) (asset) | Quantity of tokens of type X used to perform action (collateral) | $x_{A}$ |
| CollateralAsset | clpTypes.Asset | Symbol of action (external) (asset) | Quantity of tokens of type X used to perform action (collateral) | |
| Liabilities | sdk.Uint | Size of action (x) (liability | Quantity of tokens of type X in principal liabilities taken on | $x^P_{L}$ |
| InterestUnpaidCollateral | sdk.Uint | Size of action (x) (liability) | Quantity of tokens of type X in interest liabilities accrued and pending payment. Denominated in collateral asset. | $x^E_{L}$ |
| InterestPaidCollateral | sdk.Uint | Size of action (x) (liability | Quantity of tokens of type X accrued as interest and paid by position. Denominated in collateral asset. | |
| InterestPaidCustody | sdk.Uint | Size of action (x) (liability | Quantity of tokens of type X accrued as interest and paid by position. Denominated in custody asset. | |
| CustodyAmount | sdk.Uint | Size of action (y) (asset) | Quantity of tokens of type Y that are held in custody by taking on liability position in x | $y_{C}$ |
| CustodyAsset | clpTypes.Asset | Symbol of action (y) (asset) | Quantity of tokens of type Y that are held in custody by taking on liability position in x | $y_{C}$ |
| Leverage | sdk.Uint | Leverage | Leverage multiplier on amount borrowed | $\eta_{mtp}$ |
| Health | sdk.Dec | MTP Health | Metric describing the health of an individual MTP | $h(\mathbf x)$ |
### Global Margin (Stateful) Parameters
Stateful parameters are akin to the Parameter dictionary implemented in a Cosmos SDK. Values stored in the parameter dictionary may be constant, but they do not need to be. Methods to update parameter values may be implemented as well.
The Stateful Parameter set is necessary to maintain the Global State of the state variables listed above. Thus, the distinction between the two is nuanced.
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/proto/sifnode/margin/v1/types.proto#L9
| Name| Schema |
|-----|----------|
| Params| Struct |
| Variable Name | Variable Type | Descriptive Name | Description | Symbol |
| -------------------- | ------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| InterestRateMax | sdk.Dec | Maximum interest rate | Maximum interest rate that an $mtp$ can incur when it's open. Typically applies to unhealthy $mtp$'s. | $\beta_{max}$ |
| InterestRateMin | sdk.Dec | Minimum interest rate | Minimum interest rate that an $mtp$ can incur when it's open. Set by the liquidity pool and typically applies to well performing $mtp$'s. | $\beta_{min}$ |
| InterestRateIncrease | sdk.Dec | Interest Rate Change Increase | Allowable Change for Interest Rate in the Increasing Direction | $\Delta\beta_{\uparrow}$ |
| InterestRateDecrease | sdk.Dec | Interest Rate Change Decrease | Allowable Change for Interest Rate in the Decreasing Direction | $\Delta\beta_{\downarrow}$ |
| HealthGainFactor | sdk.Dec | Health Gain Factor | Gain factor used for borrow fee calculations | ${K_H}$ |
| margin_maintain_a | sdk.Dec | Margin Maintenance Threshold | The value of h(x) upon mtp creation. Not implemented. | $a$ |
| SafetyFactor | sdk.Dec | Default Threshold | Health level below which any agent in the pool can cover the defaulted MTP | $b$ |
| LeverageMax | sdk.Uint | Maximum allowed leverage | Maximum leverage allowed when opening an MTP | $\eta_{max}$ |
| opening_position_max | sdk.Uint | Maximum opening position | Maximum size of position permitted to open an MTP. Not implemented. | $\eta_{max}(X)$ |
| EpochLength | int64 | Epoch Length | Defined number of blocks (or time) where upcoming borrow rate is computed for each $mtp$ | $T$ |
| RemovalQueueThreshold | sdk.Dec | Removal Queue Threshold | Pool health level below which any removal request is queued | $\bar{H}$ |
| MaxOpenPositions | uint64 | Maximum positions open | The cap of number of open positions at which no new positions can be opened. |
| InsuranceFundAddress | string | Address of insurance fund wallet | The Sifchain address of the wallet to which insurance funds should go. |
| ForceCloseFundPercentage | sdk.Dec | Force close insurance fund proceed percentage | The percentage of proceeds from force closes that should go to insurance fund |
| PoolOpenThreshold | sdk.Dec | Pool health threshold for opens | The pool health threshold at which new positions cannot be opened |
| SqModifier | sdk.Dec | Removal queue formula modifier | The modifier value for the removal queue's SQ formula
| ClosedPools | []string | Pools that are closed | Closed pools cannot have new positions opened on them
| IncrementalInterestPaymentEnabled | bool | Incremental interest payments enabled | Toggle to enable/disable incremental interest payments. |
### Rebalancing Enabled Parameter on the Liquidity Subsystem
| Name | Schema |
| ------------------------------ | ------ |
| From_Rebalancing_Parameter_Set | Struct |
| Variable Name | Variable Type | Descriptive Name | Description | Symbol |
| ------------- | ------------- | ---------------- | ------------------------------------------------------------------ | ----------- |
| lambda_v | sdk.Dec | Lambda Validator | Rebalancing coefficent for validator subsystem, computed in module | $\lambda_v$ |
## Messages
`Agent Action Space`
### Open _mtp_
###### tags: `v1.0`
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/types/tx.pb.go#L32
```go
type MsgOpen struct {
Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"`
CollateralAsset string `protobuf:"bytes,2,opt,name=collateral_asset,json=collateralAsset,proto3" json:"collateral_asset,omitempty"`
CollateralAmount github_com_cosmos_cosmos_sdk_types.Uint `protobuf:"bytes,3,opt,name=collateral_amount,json=collateralAmount,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Uint" json:"collateral_amount"`
BorrowAsset string `protobuf:"bytes,4,opt,name=borrow_asset,json=borrowAsset,proto3" json:"borrow_asset,omitempty"`
Position Position `protobuf:"varint,5,opt,name=position,proto3,enum=sifnode.margin.v1.Position" json:"position,omitempty"`
}
```
### Close _mtp_
###### tags: `v1.0`
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/types/tx.pb.go#L138
```go
type MsgClose struct {
Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"`
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
}
```
### Force Close _mtp_
###### tags: `v1.0`
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/types/tx.pb.go#L226
```go
type MsgForceClose struct {
Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"`
MtpAddress string `protobuf:"bytes,2,opt,name=mtp_address,json=mtpAddress,proto3" json:"mtp_address,omitempty"`
Id uint64 `protobuf:"varint,3,opt,name=id,proto3" json:"id,omitempty"`
}
```
## Errors
```go
var (
ErrMTPDoesNotExist = sdkerrors.Register("margin", 1, "mtp not found")
ErrMTPInvalid = sdkerrors.Register("margin", 2, "mtp invalid")
ErrMTPDisabled = sdkerrors.Register("margin", 3, "margin not enabled for pool")
ErrUnknownRequest = sdkerrors.Register("margin", 4, "unknown request")
ErrMTPHealthy = sdkerrors.Register("margin", 5, "mtp health above force close threshold")
ErrInvalidPosition = sdkerrors.Register("margin", 6, "mtp position invalid")
ErrMaxOpenPositions = sdkerrors.Register("margin", 7, "max open positions reached")
ErrUnauthorised = sdkerrors.Register("margin", 8, "address not on whitelist")
)
```
```go
var (
ErrQueued = sdkerrors.Register("clp", 40, "Cannot process immediately, request has been queued")
ErrRemovalsBlockedByHealth = sdkerrors.Register("clp", 41, "Cannot remove liquidity due to low pool health")
)
```
## Events
`Tracing agent action execution`
### Open _mtp_ Event
###### tags: `v1.0`
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/msg_server.go#L75
```go
ctx.EventManager().EmitEvent(sdk.NewEvent("margin/mtp_open",
sdk.NewAttribute("id", strconv.FormatInt(int64(mtp.Id), 10)),
sdk.NewAttribute("position", mtp.Position.String()),
sdk.NewAttribute("address", mtp.Address),
sdk.NewAttribute("collateral_asset", mtp.CollateralAsset),
sdk.NewAttribute("collateral_amount", mtp.CollateralAmount.String()),
sdk.NewAttribute("custody_asset", mtp.CustodyAsset),
sdk.NewAttribute("custody_amount", mtp.CustodyAmount.String()),
sdk.NewAttribute("leverage", mtp.Leverage.String()),
sdk.NewAttribute("liabilities", mtp.Liabilities.String()),
sdk.NewAttribute("interest_paid", mtp.InterestPaid.String()),
sdk.NewAttribute("interest_unpaid", mtp.InterestUnpaid.String()),
sdk.NewAttribute("health", mtp.MtpHealth.String()),
))
```
### Close _mtp_ Event
###### tags: `v1.0`
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/msg_server.go#L93
```go
ctx.EventManager().EmitEvent(sdk.NewEvent("margin/mtp_close", sdk.NewAttribute("id", strconv.FormatInt(int64(closedMtp.Id), 10)),
sdk.NewAttribute("position", closedMtp.Position.String()),
sdk.NewAttribute("address", closedMtp.Address),
sdk.NewAttribute("collateral_asset", closedMtp.CollateralAsset),
sdk.NewAttribute("collateral_amount", closedMtp.CollateralAmount.String()),
sdk.NewAttribute("custody_asset", closedMtp.CustodyAsset),
sdk.NewAttribute("custody_amount", closedMtp.CustodyAmount.String()),
sdk.NewAttribute("repay_amount", repayAmount.String()),
sdk.NewAttribute("leverage", closedMtp.Leverage.String()),
sdk.NewAttribute("liabilities", closedMtp.Liabilities.String()),
sdk.NewAttribute("interest_paid", mtp.InterestPaid.String()),
sdk.NewAttribute("interest_unpaid", closedMtp.InterestUnpaid.String()),
sdk.NewAttribute("health", closedMtp.MtpHealth.String()),
))
```
### Force Close _mtp_ Event
###### tags: `v1.0`
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/events.go#L14
```go
ctx.EventManager().EmitEvent(sdk.NewEvent("margin/mtp_force_close",
sdk.NewAttribute("id", strconv.FormatInt(int64(mtp.Id), 10)),
sdk.NewAttribute("position", mtp.Position.String()),
sdk.NewAttribute("address", mtp.Address),
sdk.NewAttribute("collateral_asset", mtp.CollateralAsset),
sdk.NewAttribute("collateral_amount", mtp.CollateralAmount.String()),
sdk.NewAttribute("custody_asset", mtp.CustodyAsset),
sdk.NewAttribute("custody_amount", mtp.CustodyAmount.String()),
sdk.NewAttribute("repay_amount", repayAmount.String()),
sdk.NewAttribute("leverage", mtp.Leverage.String()),
sdk.NewAttribute("liabilities", mtp.Liabilities.String()),
sdk.NewAttribute("interest_paid", mtp.InterestUnpaid.String()),
sdk.NewAttribute("interest_unpaid", mtp.InterestUnpaid.String()),
sdk.NewAttribute("health", mtp.MtpHealth.String()),
sdk.NewAttribute("closer", closer),
))
```
## Message Server
`Permissible Agent Action Determination Posterior`
Message Server functions fulfill requests submitted through messages. Margin Message Server Functions are representations of Composite Mechanisms as outlined in the [Sifchain Cryptoeconomics Paper](https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Composite_Mechanisms). Each message server function handles routing messages through relevant Keeper Functions (Primitive Mechanisms).
###### tags: `v1.0`
```go
type msgServer struct {
Keeper
}
```
### Open _mtp_ long
###### tags: `v1.0`
Message Server Logic for Open MTP Long
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Open-mtp1
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/open_swap_custody_x.py
Experiment:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/5_open_mtp.ipynb
- Status: Merged
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/msg_server_test.go#L34
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/msg_server.go#L165
#### Open Long Keeper Function Flow
1. [CustodySwap](#Custody-Swap)
2. [Borrow](#Borrow)
3. [UpdatePoolHealth](#Update-Pool-Health)
4. [TakeInCustody](#Take-into-Custody)
#### Opening a Position
A user opens a long position with 1 Frax as collateral using 3x leverage.
```flow
st=>start: Take In Collateral
e=>end: End
op=>operation: Borrow
op2=>operation: Update Pool Health
op3=>operation: Custody Swap
op4=>operation: Take In Custody
st->op->op2->op3->op4->end
```
1. **Take In Collateral** The 1 Frax collateral is taken from the users wallet and moved into the clp module account, and added to the pool balance, as well as accounted for in the MTP.CollateralAmount property.
2. **Borrow** The borrow amount is 2 Frax, which is tracked as a pool liability, and on the MTP.LiabilityP property.
3. **Take In Custody** The collateral and liability amount is swapped to custody token and taken out of the pool. The value is stored in the pool's custody bucket and in MTP.CustodyAmount.
```go
func (k msgServer) OpenLong(goCtx context.Context, msg *types.MsgOpenLong) (*types.MsgOpenLongResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
leverageMax := k.GetLeverageMaxParam(ctx)
collateralAmount := msg.CollateralAmount
mtp := types.NewMTP(ctx, msg.Signer, msg.CollateralAsset, msg.BorrowAsset) // create func TODO
pool := clptypes.pool{}
nativeAsset := types.GetSettlementAsset()
if msg.CollateralAsset.Equals(nativeAsset) {
pool = k.clpKeeper.getPool(ctx, msg.BorrowAsset)
}
else {
pool = k.clpKeeper.getPool(ctx, msg.CollateralAsset)
}
leveragedAmount := CollateralAmount.Mul(sdk.Uint.NewUint(1).Add(leverageMax))
borrowAmount := k.CustodySwap(ctx, pool, msg.BorrowAsset, leveragedAmount)
k.Borrow(ctx, msg.CollateralAsset, collateralAmount, msg.BorrowAsset, borrowAmount, mtp, pool, leverageMax)
k.UpdatePoolHealth(ctx, pool)
k.TakeInCustody(ctx, mtp, pool)
}
```
### Close _mtp_ long
###### tags: `v1.0`
Message Server Logic for Close MTP Long
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Close-mtp1
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/close.py
Experiment:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/6_close_mtp.ipynb
- Status: Needs to be re-run
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/msg_server_test.go#L215
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/msg_server.go#L224
#### Close Long Keeper Function Flow
1. [TakeOutCustody](#Take-out-of-Custody)
2. [CustodySwap](#Custody-Swap)
3. [InterestRateComputation](#Interest-Computation)
4. [UpdateMTPInterestLiabilities](#Update-Interest-Liabilities)
5. [Repay](#Repay)
#### Voluntary Closure
```flow
start=>start: Take Out Custody
op1=>operation: Swap
op2=>operation: Update Interest
end=>end: Repay
start->op1->op2->end
```
1. The mtp custody amount is taken out of the pool’s custody bucket and placed back into the pool.
2. The custody amount is swapped back to the collateral token as a repay amount.
3. Interest is calculated
4. The repay amount is used to repay the position.
5. The user is paid any profits
6. The mtp liability is removed from the pool liability bucket (in the collateral token)
7. The traders profits or loss are removed from the pool balance (in the collateral token)
```go
func (k msgServer) CloseLong(goCtx context.Context, msg *types.MsgCloseLong) (*types.MsgCloseLongResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
mtp := k.getMTP(ctx, msg.Signer, msg.CollateralAsset, msg.BorrowAsset)
pool := clptypes.pool{}
nativeAsset := types.GetSettlementAsset()
if msg.CollateralAsset.Equals(nativeAsset) {
pool = k.clpKeeper.getPool(ctx, msg.BorrowAsset)
}
else {
pool = k.clpKeeper.getPool(ctx, msg.CollateralAsset)
}
k.TakeOutCustody(ctx, mtp, pool)
repayAmount := k.CustodySwap(ctx, pool, mtp.CollateralAsset, mtp.CustodyAmount)
// interest comp
interestRate = k.InterestRateComputation(ctx, pool)
k.UpdateMTPInterestLiabilities(ctx, mtp, interestRate)
repay := mtp.Repay(ctx, mtp, pool, repayAmount)
}
```
### Force Close _mtp_ long
###### tags: `v1.0`
Message Server Logic for Force Close MTP Long
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Forced-Settlement-by-Keeper
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/2101d487c9c39cd9137ad3036198d45ebdf3ebe3/model/parts/keeper_maintainence.py#L7
Experiment:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/EC7_force_close_address.ipynb
- Status: ?
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/msg_server_test.go#L443
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/msg_server.go#L273
#### Force Close Long Keeper Function Flow
1. [InterestRateComputation](#Interest-Computation)
2. [UpdateMTPInterestLiabilities](#Update-Interest-Liabilities)
3. [UpdateMTPHealth](#Update-MTP-Health)
4. [TakeOutCustody](#Take-out-of-Custody)
5. [CustodySwap](#Custody-Swap)
6. [Repay](#Repay)
#### Force Closure
```flow
start=>start: Update Interest
op1=>operation: Take Out Custody
op2=>operation: Swap
end=>end: Repay
start->op1->op2->end
```
```go
func (k Keeper) ForceCloseLong(ctx sdk.Context, msg *types.MsgForceClose) (*types.MTP, error) {
mtp, err := k.GetMTP(ctx, msg.MtpAddress, msg.Id)
var pool clptypes.Pool
nativeAsset := types.GetSettlementAsset()
if strings.EqualFold(mtp.CollateralAsset, nativeAsset) {
pool, err = k.ClpKeeper().GetPool(ctx, mtp.CustodyAsset)
} else {
pool, err = k.ClpKeeper().GetPool(ctx, mtp.CollateralAsset)
}
// check MTP health against threshold
forceCloseThreshold := k.GetForceCloseThreshold(ctx)
interestRate, err := k.InterestRateComputation(ctx, pool)
err = k.UpdateMTPInterestLiabilities(ctx, &mtp, interestRate)
mtpHealth, err := k.UpdateMTPHealth(ctx, mtp, pool)
if err != nil {
return nil, err
}
if mtpHealth.GT(forceCloseThreshold) {
return nil, sdkerrors.Wrap(types.ErrMTPHealthy, msg.MtpAddress)
}
err = k.TakeOutCustody(ctx, mtp, &pool)
repayAmount, err := k.CustodySwap(ctx, pool, mtp.CollateralAsset, mtp.CustodyAmount)
err = k.Repay(ctx, &mtp, pool, repayAmount)
return &mtp, nil
}
```
### Open _mtp_ short
###### tags: `WIP`
Message Server Logic for Open MTP Short
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Open-Short
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/open_swap_custody_x.py
```go
func (k msgServer) OpenShort(goCtx context.Context, msg *types.MsgOpenShort) (*types.MsgOpenShortResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
eta := k.GetEta(ctx)
delta_x := msg.delta_x
mtp := k.createMTP(ctx, msg.Signer, msg.asset)
pool := k.clpKeeper.getPool(ctx, msg.asset)
ExternalAssets := pool.X_Balance
NativeAssets := pool.Y_Balance
tot_x := delta_x.Mul(sdk.Uint.NewUint(1).Add(eta)
tot_y := k.CustodySwap(ExternalAssets, NativeAssets, tot_x)
k.Borrow(ctx, mtp, delta_x, eta) # sets xa, xl, and h
k.UpdatePoolHealth(ctx, pool)
k.TakeInCustody(ctx, mtp, pool, tot_y)
ExternalCustody = k.CustodySwap(NativeAssets, ExternalAssets, tot_y)
k.ShortCustody(ctx, mtp, pool, ExternalCustody, tot_y)
}
```
### Open _mtp_ long other pool
###### tags: `WIP`
Message Server Logic for Open MTP Double Swap Long
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Double-Swap-Long
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/open_double_swap.py
Experiment:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/jesse/double_swap_long.ipynb
- Status: Merged
```go
func (k msgServer) OpenLongDouble(goCtx context.Context, msg *types.MsgOpenLongDouble) (*types.MsgOpenLongDoubleResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
eta := k.GetEta(ctx)
delta_x := msg.delta_x
mtp := k.createMTP(ctx, msg.Signer, msg.asset_x) // change key to account for both assets?
pool_x := k.clpKeeper.getPool(ctx, msg.asset_x)
pool_z := k.clpKeeper.getPool(ctx, msg.asset_z)
ExternalAssets := pool_x.X_Balance
NativeAssets := pool_x.Y_Balance
tot_x := delta_x.Mul(sdk.Uint.NewUint(1).Add(eta)
tot_y := k.CustodySwap(ExternalAssets, NativeAssets, tot_x)
k.Borrow(ctx, mtp, delta_x, eta) # sets xa, xl, and h
k.UpdatePoolHealth(ctx, pool_x)
k.TakeInCustody(ctx, mtp, pool_x, tot_y)
Z_A := pool_z.Z_Balance
NativeAssetsPool2 := pool_z.Y_Balance // TODO rename?
tot_z := k.CustodySwap(NativeAssetsPool2, Z_A, tot_y)
k.TakeInCustodyDouble(ctx, mtp, pool_x, pool_z, tot_y, tot_z)
k.UpdatePoolHealthDouble(ctx) // TODO
}
```
### Open _mtp_ short other pool
###### tags: `WIP`
Message Server Logic for Open MTP Short Double
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Double-Swap-Short
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/open_double_swap.py
Experiment:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/14_double_swap_short_open.ipynb
- Status: Merged
```go
func (k msgServer) OpenShortDouble(goCtx context.Context, msg *types.MsgOpenShortDouble) (*types.MsgOpenShortDoubleResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
eta := k.GetEta(ctx))
delta_x := msg.delta_x
mtp := k.createMTP(ctx, msg.Signer, msg.asset_x) // change key to account for both assets?
pool_x := k.clpKeeper.getPool(ctx, msg.asset_x)
pool_z := k.clpKeeper.getPool(ctx, msg.asset_z)
ExternalAssets := pool_x.X_Balance
NativeAssets := pool_x.Y_Balance
tot_x := delta_x.Mul(sdk.Uint.NewUint(1).Add(eta)
tot_y_x := k.CustodySwap(ExternalAssets, NativeAssets, tot_x)
k.Borrow(ctx, mtp, delta_x, eta) # sets xa, xl, and h
k.UpdatePoolHealth(ctx, pool_x)
k.TakeInCustody(ctx, mtp, pool_x, tot_y)
Z_A := pool_z.X_Balance
NativeAssetsPool2 := pool_z.Y_Balance
// calculate delta_z from tot_y
tot_z := k.CustodySwap(NativeAssetsPool2, Z_A, tot_y)
tot_y_z := k.CustodySwap(Z_A, NativeAssetsPool2, tot_z)
ExternalCustody := k.CustodySwap(NativeAssetsPool2, ExternalAssets, tot_y_z)
k.ShortCustodyDouble(ctx, mtp, pool_x, pool_z, tot_y_x, tot_y_z, tot_z, ExternalCustody)
k.UpdatePoolHealthDouble(ctx) // TODO
}
```
### Close _mtp_ short
###### tags: `WIP`
Message Server Logic for Close MTP Short
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Close-mtp1
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/close.py
Experiment:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/15c_open_close_short_y.ipynb
- Status: Merged
```go
func (k msgServer) CloseShort(goCtx context.Context, msg *types.MsgCloseShort) (*types.MsgCloseShortResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
mtp := k.getMTP(ctx, msg.Signer, msg.asset)
pool := k.clpKeeper.getPool(ctx, msg.asset)
ExternalAssets := pool.X_Balance
NativeAssets := pool.Y_Balance
ExternalLiabilities := pool.ExternalLiabilities
NativeLiabilities := pool.NativeLiabilities
X := ExternalAssets.Add(ExternalLiabilities)
Y := NativeAssets.Add(NativeLiabilities)
delta_x := msg.delta_x
k.TakeOutCustody(ctx, mtp, pool, delta_x)
delta_y := k.CustodySwap(Y, X, delta_x)
mtp.Repay(ctx, mtp, pool, delta_y, closure, maintenance)
k.UpdateHealth(ctx, mtp)
}
```
### Margin maintenance - Pay off Interest Liabilities
###### tags: `WIP`
Message Server Logic for Margin Maintenance - Pay off Interest Liabilities: Reduce Liability
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Margin-Maintenance--Pay-off-Interest-Liabilities
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/margin_maintenance.py
```go
func (k msgServer) ReduceLiabilityMaint(goCtx context.Context, msg *types.MsgReduceLiabilityMaint) (*types.MsgReduceLiabilityMaintResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
mtp := k.getMTP(msg.Signer, msg.asset)
pool := k.clpKeeper.getPool(ctx, msg.asset)
ExternalAssets := pool.X_Balance
NativeAssets := pool.Y_Balance
ExternalLiabilities := pool.ExternalLiabilities
NativeLiabilities := pool.NativeLiabilities
repayAmount := msg.repayAmount
ExternalLiabilitiesP := mtp.ExternalLiabilitiesP
ExternalLiabilitiesI := mtp.ExternalLiabilitiesI
// interest liability comp TODO
k.MaintenanceRepay(ctx, mtp, pool, repayAmount)
k.UpdateMTPHealth(ctx, mtp)
}
```
### Margin maintenance - Pay off Interest Liabilities
###### tags: `WIP`
Message Server Logic for Margin Maintenance - Pay off Interest Liabilities: Return Borrowed Tokens
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Margin-Maintenance--Pay-off-Interest-Liabilities
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/margin_maintenance.py
```go
func (k msgServer) ReturnBorrowMaint(goCtx context.Context, msg *types.MsgReturnBorrowMaint) (*types.MsgReturnBorrowMaintResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
mtp := k.getMTP(ctx, msg.Signer, msg.asset)
pool := k.clpKeeper.getPool(ctx, msg.asset)
ExternalAssets := pool.X_Balance
NativeAssets := pool.Y_Balance
ExternalLiabilities := pool.ExternalLiabilities
NativeLiabilities := pool.NativeLiabilities
repayAmount := msg.repayAmount
ExternalLiabilitiesP := mtp.ExternalLiabilitiesP
ExternalLiabilitiesI := mtp.ExternalLiabilitiesI
if msg.addCollateral {
X := ExternalAssets.Add(ExternalLiabilities)
Y := NativeAssets.Add(NativeLiabilities)
delta_y := msg.delta_y
k.TakeOutCustody(ctx, mtp, pool, delta_y)
repayAmount := k.CustodySwap(X, Y, delta_y) // swap close
}
// interest liability comp TODO
k.MaintenanceRepay(ctx, mtp, pool, repayAmount)
k.UpdateMTPHealth(ctx, mtp)
}
```
## Keeper Functions
Margin Keeper Functions are representations of Primitive Mechanisms as outlined in the [Sifchain Cryptoeconomics Paper](https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Primitive-Mechanisms).
### Borrow
The Borrow function is responsible for updating accounting in both MTP and Pool objects for a borrow action. It also handles the transfer of collateral coins from the MTP holder to the pool.
###### tags: `v1.0`
Global State Transition for Borrow
Mechanism Spec: https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Borrow
Reference Python cadCAD: https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/mtp.py
Experiment:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/5_open_mtp.ipynb
- Status: Merged
Test Spec: https://hackmd.io/b3R1-ms8SECslTFkyhmwOA?view#Borrow
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L231
```go
/*
Borrow updates the mtp collateral, and liabilities (based on leverage amount).
The pool balance and liabilities are updated, and the funds are moved from mtp address to module.
At this point the mtp health is also calculated.
*/
func (k Keeper) Borrow(ctx sdk.Context, collateralAsset clpTypes.Asset, collateralAmount sdk.Uint, borrowAmount sdk.Uint, mtp MTP, pool clpTypes.Pool, leverage sdk.Uint) error {
mtp.CollateralAmount = mtp.CollateralAmount.Add(collateralAmount)
mtp.LiabilitiesP = mtp.ExternalLiabilitiesP.Add(collateralAmount.Mul(leverage))
mtp.CustodyAssets = mtp.CustodyAssets.Add(borrowAmount)
mtp.Leverage = leverage
nativeAsset := clpTypes.GetSettlementAsset()
if collateralAsset.Equals(nativeAsset) {
pool.NativeAssetBalance = pool.NativeAssetBalance.Add(collateralAmount)
pool.NativeLiabilities = poolNativeLiabilities.Add(collateralAmount.Mul(leverage))
}
else {
pool.ExternalAssetBalance = pool.ExternalAssetBalance.Add(collateralAmount)
pool.ExternalLiabilities = pool.ExternalLiabilities.Add(collateralAmount.Mul(leverage))
}
mtp.h = k.UpdateMTPHealth(ctx, mtp, pool) // set mtp in func or return h?
var coins sdk.Coins
collateralCoin := sdk.NewCoin(collateralAsset.Symbol, collateralAmount)
collateralcoins := coins.Add(collateralCoin)
k.supplyKeeper.SendCoinsFromAccountToModule(ctx, mtp.Address, types.ModuleName, collateralcoins)
clpkeeper.SetPool(ctx, pool)
mtpkeeper.SetMTP(ctx, mtp)
}
```
### Repay
The Repay function is responsible for updating accounting in both MTP and Pool objects for a repay action. Repay uses an input amount of assets to settle accumulated debt in an MTP. The function first pays off principle liabilities, then interest liabilities, and any excess assets are paid back to the MTP holder as profit.
For force closures, a percentage of the proceeds can go to a wallet address specified by governance. The percentage is also a governance parameter.
###### tags: `v1.0`
Global State Transition for Repay
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Repay
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/mtp.py
Experiment:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/6_close_mtp.ipynb
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L358
- Status: Merged
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/keeper_test.go#L663
```go
func (k Keeper) Repay(ctx sdk.Context, mtp MTP, pool clpTypes.Pool, repayAmount sdk.Uint, takeInsurance bool) error {
returnAmount, debtP, debtI := sdk.ZeroUint()
LiabilitiesP := mtp.LiabilitiesP
LiabilitiesI := mtp.LiabilitiesI
mtp.h = k.UpdateMTPHealth(ctx, mtp, pool)
have := repayAmount
owe := LiabilitiesP.Add(LiabilitiesI)
if have.LT(LiabilitiesP) {
//can't affort principle liability
returnAmount = sdk.ZeroUint()
debtP = LiabilitiesP - have
debtI = LiabilitiesI
}
else if have.LT(owe) {
// v principle liability; x excess liability
returnAmount = sdk.ZeroUint()
debtP = sdk.ZeroUint()
debtI = LiabilitiesP.Add(LiabilitiesI).Sub(have)
}
else {
// can afford both
returnAmount = have.Sub(LiabilitiesP).Sub(LiabilitiesI)
debtP = sdk.ZeroUint()
debtI = sdk.ZeroUint()
}
if !returnAmount.IsZero() {
actualReturnAmount := returnAmount
if takeInsurance {
takePercentage := k.GetForceCloseFundPercentage(ctx)
returnAmountDec := sdk.NewDecFromBigInt(returnAmount.BigInt())
takeAmount := sdk.NewUintFromBigInt(takePercentage.Mul(returnAmountDec).TruncateInt().BigInt())
actualReturnAmount = returnAmount.Sub(takeAmount)
if !takeAmount.IsZero() {
takeCoins := sdk.NewCoins(sdk.NewCoin(mtp.CollateralAsset, sdk.NewIntFromBigInt(takeAmount.BigInt())))
fundAddr := k.GetInsuranceFundAddress(ctx)
err = k.BankKeeper().SendCoinsFromModuleToAccount(ctx, clptypes.ModuleName, fundAddr, takeCoins)
if err != nil {
return err
}
k.EmitRepayInsuranceFund(ctx, mtp, takeAmount)
}
}
if !actualReturnAmount.IsZero() {
var coins sdk.Coins
returnCoin := sdk.NewCoin(collateralAsset.Symbol, returnAmount)
returnCoins := coins.Add(returnCoin)
k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, mtp.Address, returnCoins)
}
}
nativeAsset := clpTypes.GetSettlementAsset()
if mtp.CollateralAsset.Equals(nativeAsset) {
pool.NativeAssetBalance = pool.NativeAssetBalance.Sub(debtI).Sub(debtP)
pool.NativeLiabilities = pool.NativeLiabilities.Sub(mtp.LiabilitiesP)
}
else {
pool.ExternalAssetBalance = pool.NativeAssetBalance.Sub(debtI).Sub(debtP)
pool.ExternalLiabilities = pool.NativeLiabilities.Sub(mtp.LiabilitiesP)
}
mtpkeeper.DestroyMTP(ctx, mtp)
k.clpKeeper.SetPool(ctx, pool)
}
```
### Take into Custody
The Take into Custody function is responsible for updating accounting in a Pool object when assets are entered into custody.
###### tags: `v1.0`
Global State Transition for Take into Custody
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Take_into_Custody
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/mtp.py
Experiment: https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/5_open_mtp.ipynb
- Status: Merged
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/keeper_test.go#L571
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L330
```go
func (k Keeper) TakeInCustody(ctx sdk.Context, mtp MTP, pool clpTypes.Pool) error {
nativeAsset := clpTypes.GetSettlementAsset()
if mtp.CollateralAsset.Equals(nativeAsset) {
pool.ExternalAssetBalance = pool.ExternalAssetBalance.Sub(mtp.CustodyAmount)
pool.ExternalCustody = pool.ExternalCustody.Add(mtp.CustodyAmount)
}
else {
pool.NativeAssetBalance = pool.NativeAssetBalance.Sub(mtp.CustodyAmount)
pool.NativeCustody = pool.NativeCustody.Add(mtp.CustodyAmount)
}
k.clpKeeper.SetPool(ctx, pool)
}
```
### Take out of Custody
The Take out of Custody function is responsible for updating accounting in a Pool object when assets are released from protocol custody.
###### tags: `v1.0`
Global State Transition for Take into Custody
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Take_out_of_Custody
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/d28083fe6191c6d88d4f33ce5a4e2e1497d58b0d/model/parts/clp.py#L41
Experiment:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/6_close_mtp.ipynb
- Status: Merged
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/keeper_test.go#L617
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L344
```go
func (k Keeper) TakeOutCustody(ctx sdk.Context, mtp MTP, pool clpTypes.Pool) error {
nativeAsset := clpTypes.GetSettlementAsset()
if mtp.CollateralAsset.Equals(nativeAsset) {
pool.ExternalCustody = pool.ExternalCustody.Sub(mtp.CustodyAmount)
}
else {
pool.NativeCustody = pool.NativeCustody.Sub(mtp.CustodyAmount)
}
k.clpKeeper.SetPool(ctx, pool)
}
```
### Swap
Margin utilizes CLP swap functions to calculate custody swap results.
###### tags: `v1.0`
Calculation Function for Custody Swap
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Swap_Open_mtp
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/open_swap_custody_x.py
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/keeper_test.go#L172
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L218
```go
func (k Keeper) CLPSwap(ctx sdk.Context, sentAmount sdk.Uint, to string, pool clptypes.Pool) (sdk.Uint, error) {
toAsset := ToAsset(to)
// add liabilities? and custody to pool depth
pool.NativeAssetBalance = pool.NativeAssetBalance.Add(pool.NativeCustody).Add(pool.NativeLiabilities)
pool.ExternalAssetBalance = pool.ExternalAssetBalance.Add(pool.ExternalCustody).Add(pool.ExternalLiabilities)
swapResult, err := k.ClpKeeper().CLPCalcSwap(ctx, sentAmount, toAsset, pool)
if err != nil {
return sdk.Uint{}, err
}
return swapResult, nil
}
```
### Health and Interest Functions
Health and interest functions are called both by [Message Server Functions](#Message-Server) and by [Periodic Health and Interest Updates](#Periodic-Health-and-Interest-Updates) through BeginBlocker. These functions compute updates to health
#### Update MTP Health
The Update MTP Health function is responsible for computing updates to an individual MTP's health metric. The health metric represents a position's ability to cover its liabilities with its collateral.
###### tags: `v1.0`
Global State Transition for MTP Health Calculation
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#mtp_Health_Computation
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/health.py
Experiment: EC9 tests 1-3
* Status: Merged
* [Test 1](https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/EC9_test1.ipynb)
* [Test 2](https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/EC9_test2.ipynb)
* [Test 3](https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/EC9_test3.ipynb)
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/keeper_test.go#L440
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L308
```go
/*
MTP health is calculated as a ratio of custody to debt.
The custody swap function is used to normalise the values.
*/
// in this model everything is calculated in y price, you can treat y as
// a "inter protocle settlement currency", Sifchain team is kind of a
// central bank that controls inflation.
// does not account for double position
func (k Keeper) UpdateMTPHealth(ctx sdk.Context, mtp MTP, pool clpTypes.Pool) sdk.Uint, error {
yc := sdk.NewDecFromBigInt(mtp.CustodyAmount.BigInt())
xlp := mtp.LiabilitiesP
if xlp.IsZero() {
return sdk.ZeroDec(), nil
}
debt, err := k.CLPSwap(ctx, xlp, mtp.CustodyAsset, pool)
if err != nil {
return sdk.ZeroDec(), nil
}
lr := yc.Quo(sdk.NewDecFromBigInt(debt.BigInt()))
return lr, nil
}
```
#### Update Pool Health
The Update Pool Health function is responsible for computing updates to an individual Pool's health metric. The health metric represents a ratio between the pool's assets and liabilities.
###### tags: `v1.0`
Global State Transition for Pool Health Calculation
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Pool_Health_Computation
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/add_colat.py
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/keeper_test.go#L420
EC8 tests 1-3
* Status: Merged
* [Test 1]( https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/EC8_test1.ipynb)
* [Test 2](https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/EC8_test2.ipynb)
* [Test 3](https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/EC8_test3.ipynb)
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L288
```go
/*
The pool health is calculated as function of the pools (external and native) assets and liabilities.
It is a ratio of the balance over the sum of balance and liabilities.
*/
func (k Keeper) UpdatePoolHealth(ctx sdk.Context, pool clpTypes.Pool) error {
// can be both X and Y
ExternalAssetBalance := pool.ExternalAssetBalance
ExternalLiabilities := pool.ExternalLiabilities
NativeAssetBalance := pool.NativeAssetBalance
NativeLiabilities := pool.NativeLiabilities
mul1 := ExternalAssetBalance.Quo(ExternalAssetBalance.Add(ExternalLiabilities))
mul2 := NativeAssetBalance.Quo(NativeAssetBalance.Add(NativeLiabilities))
H := mul1.Mul(mul2)
pool.Health = H
k.clpKeeper.SetPool(ctx, pool)
}
```
#### Interest Computation
The Interest Computation function is responsible for computing updates to an individual Pool's interest rate. The interest rate result is a function of the pool's health and a parameterized gain factor.
###### tags: `v1.0`
Calculation Function for Interest Rate
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Target-Borrow-Fee-Domain-Mapping-from-Health-Metrics
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/interest_rate.py
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L501
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/keeper_test.go#L870
```go
/*
When calculating a change to the interest rate, a mechanism is in place to cap the change increase or decrease according to parameters, as well as cap the max or min interest rate.
The interest rate is derived from the health gain factor multiplied by a formula that is similar to the inverse of the pool health calculation (assets and liabilities over assets).
*/
func (k Keeper) InterestRateComputation(ctx sdk.Context, pool clpTypes.Pool) (sdk.Dec, error) {
interestRateMax := k.GetInterestRateMax(ctx)
interestRateMin := k.GetInterestRateMin(ctx)
interestRateIncrease := k.GetInterestRateIncrease(ctx)
interestRateDecrease := k.GetInterestRateDecrease(ctx)
healthGainFactor := k.GetHealthGainFactor(ctx)
prevInterestRate := pool.InterestRate
mul1 := pool.ExternalAssetBalance.Add(pool.ExternalLiabilities).Quo(pool.ExternalAssetBalance)
mul2 := pool.NativeAssetBalance.Add(pool.NativeLiabilities).Quo(pool.NativeAssetBalance)
targetInterestRate := healthGainFactor.Mul(mul1).Mul(mul2)
interestRateChange := targetInterestRate.Sub(prevInterestRate)
interestRate := prevInterestRate
if interestRateChange.LTE(interestRateDecrease.Mul(sdk.NewDec(-1))) && interestRateChange.LTE(interestRateIncrease) {
interestRate = targetInterestRate
}
else if interestRateChange.GT(interestRateIncrease) {
interestRate = prevInterestRate.Add(interestRateIncrease)
}
else if interestRateChange.LT(interestRateDecrease.Mul(sdk.NewDec(-1))) {
interestRate = prevInterestRate.Sub(interestRateDecrease)
}
newInterestRate = interestRate
if interestRate.GT(interestRateMin) && interestRate.LT(interestRateMax) {
newInterestRate = interestRate
}
else if interestRate.LTE(interestRateMin) {
newInterestRate = interestRateMin
}
else if interestRate.GTE(interestRateMax) {
newInterestRate = interestRateMax
}
return newInterestRate, nil
}
```
#### Update Interest Liabilities
The Interest Computation function is responsible for computing updates to an individual MTP's interest liabilities.
###### tags: `v1.0`
Global State Transition for MTP Interest Liabilities
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Interest_Liability_Computation_on_span-idMathJax-Element-68-Frame-classmjx-chtml-MathJax_CHTML-tabindex0-stylefont-size-113-position-relative-data-mathmlmtp-rolepresentationmtpmtpmtp
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/mtp.py
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/calculations.go#L13
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/keeper_test.go#L840
```go
/*
When updating interest liabilities for a margin position, the interest rate is applied to the sum of the principle liabilties and interest liabilities.
*/
func CalcMTPInterestLiabilities(mtp *types.MTP, interestRate sdk.Dec, epochPosition, epochLength int64) sdk.Uint {
var interestRational, liabilitiesRational, rate, epochPositionRational, epochLengthRational big.Rat
rate.SetFloat64(interestRate.MustFloat64())
liabilitiesRational.SetInt(mtp.Liabilities.BigInt().Add(mtp.Liabilities.BigInt(), mtp.InterestUnpaid.BigInt()))
interestRational.Mul(&rate, &liabilitiesRational)
if epochPosition > 0 { // prorate interest if within epoch
epochPositionRational.SetInt64(epochPosition)
epochLengthRational.SetInt64(epochLength)
epochPositionRational.Quo(&epochPositionRational, &epochLengthRational)
interestRational.Mul(&interestRational, &epochPositionRational)
}
interestNew := interestRational.Num().Quo(interestRational.Num(), interestRational.Denom())
return sdk.NewUintFromBigInt(interestNew.Add(interestNew, mtp.InterestUnpaid.BigInt()))
}
```
#### IncrementalInterestPayment
###### tags: `v1.0`
`Keeper Function`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L445
Pay off interestPayment argument using mtp collateral.
The interest payment is calculated in the collateral asset. It is then swapped to the custody asset and deducted from the position's custody. The payment is then deducted from the pool's custody and added back to the pool balance, minus any insurance fund percentage.
```go
func (k Keeper) IncrementalInterestPayment(ctx sdk.Context, interestPayment sdk.Uint, mtp *types.MTP, pool clptypes.Pool) (sdk.Uint, error) {
// if mtp has unpaid interest, add to payment
if mtp.InterestUnpaidCollateral.GT(sdk.ZeroUint()) {
interestPayment = interestPayment.Add(mtp.InterestUnpaidCollateral)
}
// swap interest payment to custody asset for payment
interestPaymentCustody, err := k.CLPSwap(ctx, interestPayment, mtp.CustodyAsset, pool)
if err != nil {
return sdk.ZeroUint(), err
}
// if paying unpaid interest reset to 0
mtp.InterestUnpaidCollateral = sdk.ZeroUint()
// edge case, not enough custody to cover payment
if interestPaymentCustody.GT(mtp.CustodyAmount) {
// swap custody amount to collateral for updating interest unpaid
custodyAmountCollateral, err := k.CLPSwap(ctx, mtp.CustodyAmount, mtp.CollateralAsset, pool) // may need spot price here to not deduct fee
if err != nil {
return sdk.ZeroUint(), err
}
mtp.InterestUnpaidCollateral = interestPayment.Sub(custodyAmountCollateral)
interestPayment = custodyAmountCollateral
interestPaymentCustody = mtp.CustodyAmount
}
// add payment to total paid - collateral
mtp.InterestPaidCollateral = mtp.InterestPaidCollateral.Add(interestPayment)
// add payment to total paid - custody
mtp.InterestPaidCustody = mtp.InterestPaidCustody.Add(interestPaymentCustody)
// deduct interest payment from custody amount
mtp.CustodyAmount = mtp.CustodyAmount.Sub(interestPaymentCustody)
takePercentage := k.GetIncrementalInterestPaymentFundPercentage(ctx)
fundAddr := k.GetIncrementalInterestPaymentInsuranceFundAddress(ctx)
takeAmount, err := k.TakeInsurance(ctx, interestPaymentCustody, mtp.CustodyAsset, takePercentage, fundAddr)
if err != nil {
return sdk.ZeroUint(), err
}
actualInterestPaymentCustody := interestPaymentCustody.Sub(takeAmount)
if !takeAmount.IsZero() {
k.EmitInsuranceFundPayment(ctx, mtp, takeAmount, mtp.CustodyAsset, types.EventIncrementalPayInsuranceFund)
}
nativeAsset := types.GetSettlementAsset()
if types.StringCompare(mtp.CustodyAsset, nativeAsset) { // custody is native
pool.NativeCustody = pool.NativeCustody.Sub(interestPaymentCustody)
pool.NativeAssetBalance = pool.NativeAssetBalance.Add(actualInterestPaymentCustody)
} else { // custody is external
pool.ExternalCustody = pool.ExternalCustody.Sub(interestPaymentCustody)
pool.ExternalAssetBalance = pool.ExternalAssetBalance.Add(actualInterestPaymentCustody)
}
err = k.SetMTP(ctx, mtp)
if err != nil {
return sdk.ZeroUint(), err
}
return interestPayment, k.ClpKeeper().SetPool(ctx, &pool)
}
```
### Maintenance Repay
###### tags: `WIP`
Global State Transition for Maintenance Repay
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Maintenance_Repay
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/mtp.py
Experiment:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/experiments/8_maint_repay.ipynb
- Status: Merged
Test Spec: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/keeper/keeper_test.go#L663
```go
func (k Keeper) MaintenanceRepay(ctx sdk.Context, mtp MTP, pool clpTypes.Pool, repayAmount sdk.Uint) error {
MTPExternalAssets := mtp.ExternalAssets
ExternalLiabilitiesP := mtp.ExternalLiabilitiesP
ExternalLiabilitiesI := mtp.ExternalLiabilitiesI //get from interest calc TODO
if repayAmount.LTE(ExternalLiabilitiesI) { // add ExternalLiabilitiesI like above
ExternalLiabilitiesI = ExternalLiabilitiesI.Sub(repayAmount)
}
// If owner pays off more than their excess liab, then principal liabilities next
else if repayAmount.LTE(ExternalLiabilitiesP.Add(ExternalLiabilitiesI) {
diff := repayAmount.Sub(ExternalLiabilitiesI)
ExternalLiabilitiesI = 0
ExternalLiabilitiesP = ExternalLiabilitiesP.Sub(diff)
}
else {
diff := repayAmount.Sub(ExternalLiabilitiesI.Sub(ExternalLiabilitiesP))
ExternalLiabilitiesI = 0
ExternalLiabilitiesP = 0
MTPExternalAssets = MTPExternalAssets.Add(diff)
}
mtp.ExternalLiabilitiesP = ExternalLiabilitiesP
mtp.ExternalLiabilitiesI = ExternalLiabilitiesI
mtp.ExternalAssets = MTPExternalAssets
k.UpdateMTPHealth(ctx, mtp)
k.SetMTP(ctx, mtp)
pool.X_Balance = pool.X_Balance.Add(repayAmount)
pool.ExternalLiabilities = pool.ExternalLiabilities.Sub(repayAmount)
if repayAmount.LTE(ExternalLiabilitiesI) {
pool.ExternalAssets = pool.ExternalAssets.Add(repayAmount)
}
else if repayAmount.LET(ExternalLiabilitiesI.Sub(ExternalLiabilitiesP) {
pool.ExternalAssets = pool.ExternalAssets.Add(repayAmount)
pool.ExternalLiabilities = pool.ExternalLiabilities.Sub(repayAmount)
}
k.clpKeeper.SetPool(ctx, pool)
}
```
### Short Custody
###### tags: `WIP`
Global State Transition for Short Custody
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Custody-Swap-Mechanism-to-Short-in-X-Y
```go
func (k Keeper) ShortCustody(ctx sdk.Context, mtp MTP, pool clpTypes.Pool, ExternalCustody sdk.Uint, tot_y sdk.Uint) error {
pool.Y_Balance = pool.Y_Balance.Add(tot_y)
pool.NativeLiabilities = pool.NativeLiabilities.Sub(tot_y)
pool.NativeCustody = pool.NativeCustody.Sub(tot_y)
pool.X_Balance -= pool.X_Balance.Sub(ExternalCustody)
pool.ExternalLiabilities = pool.ExternalLiabilities.Add(ExternalCustody)
pool.ExternalCustody = pool.ExternalCustody.Add(ExternalCustody)
mtp.NativeAssets = 0
mtp.ExternalAssets = ExternalCustody
k.clpKeeper.SetPool(ctx, pool)
k.SetMTP(ctx, mtp)
}
```
### Take into Custody Double
###### tags: `WIP`
Global State Transition for Take into Custody Double
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Custody-Swap-Mechanism-in-Y-Z
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/mtp.py#L92
```go
func (k Keeper) TakeInCustodyDouble(ctx sdk.Context, mtp MTP, pool_x sdk.Uint, pool_z sdk.Uint, tot_y sdk.Uint, tot_z sdk.Uint) error {
pool_x.NativeCustody = pool_x.NativeCustody.Sub(tot_y)
pool_z.Y_Balance = pool_z.Y_Balance.Add(tot_y)
pool_z.X_Balance = pool_z.X_Balance.Sub(tot_z)
pool_z.ExternalCustody = pool_z.ExternalCustody.Add(tot_z)
mtp.NativeAssets = mtp.NativeAssets.Sub(tot_y)
mtp.Z_A = mtp.Z_A.Add(tot_z)
k.clpKeeper.SetPool(ctx, pool_x)
k.clpKeeper.SetPool(ctx, pool_z)
k.SetMTP(mtp)
}
```
### Short Custody Double
###### tags: `WIP`
Global State Transition for Short Custody Double
Mechanism Spec:
https://hackmd.io/YJTLw7vWSLSDBZoDQDkOKw?view#Double-Custody-Swap-Mechanism-to-Short
Reference Python cadCAD:
https://github.com/Sifchain/margin_trading_cadCAD/blob/main/model/parts/mtp.py#L92
```go
func (k Keeper) ShortCustodyDouble(ctx sdk.Context, mtp MTP, pool_x sdk.Uint, pool_z sdk.Uint, tot_y_x sdk.Uint, tot_y_z sdk.Uint, tot_z sdk.Uint, ExternalCustody sdk.Uint) error {
pool_x.X_Balance = pool_x.X_Balance.Sub(ExternalCustody)
pool_x.ExternalCustody = pool_x.ExternalCustody.Add(ExternalCustody)
pool_x.Y_Balance = pool_x.Y_Balance.Add(tot_y_z.Sub(tot_y_x))
pool_z.Y_Balance = pool_z.Y_Balance.Sub(tot_y_z)
pool_z.X_Balance = pool_z.X_Balance.Sub(tot_z)
mtp.NativeAssets = 0
mtp.Z_A = 0
mtp.ExternalCustody = ExternalCustody
k.clpKeeper.SetPool(ctx, pool_x)
k.clpKeeper.SetPool(ctx, pool_z)
k.SetMTP(ctx, mtp)
}
```
## Periodic Health and Interest Updates
The Margin module runs periodic computations to update health metrics for each active MTP and Pool as well as interest rates for each Pool. During these updates the module also checks for MTPs with health metric values under the current Force Close Threshold and Force Closes unhealthy positions.
### BeginBlocker
###### tags: `v1.0`
`Epoch Action Space`
Sifchain Implementation:
https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/abci.go#L14
Update health for MTPs and Pools, and recalculate interest rate every epoch.
```flow
start=>start: Foreach Pool
updatePool=>operation: Update Pool Health + Interest Rate
foreachMTP=>operation: Foreach MTP
updateMTP=>operation: Update MTP Health + Interest Liability
maybeClose=>operation: Maybe Force Close MTP
maybeCloseEnd=>condition: More MTPs
end=>end: End
start->updatePool->foreachMTP->updateMTP->maybeClose->maybeCloseEnd
maybeCloseEnd(yes)->foreachMTP
maybeCloseEnd(no)->end
```
```go
func (k Keeper) BeginBlocker(ctx sdk.Context) {
//check if epoch has passed then execute
currentHeight := ctx.BlockHeight()
epochLength := k.GetEpochLength(ctx)
if currentHeight%epochLength == 0 { // if epoch has passed
pools := k.ClpKeeper().GetPools(ctx)
for _, pool := range pools {
if k.IsPoolEnabled(ctx, pool.ExternalAsset.Symbol) {
rate, err := k.InterestRateComputation(ctx, *pool)
if err != nil {
ctx.Logger().Error(err.Error())
continue // ?
}
pool.InterestRate = rate
_ = k.UpdatePoolHealth(ctx, pool)
k.TrackSQBeginBlock(ctx, pool)
_ = k.clpKeeper.SetPool(ctx, pool)
mtps, _, _ := k.GetMTPsForPool(ctx, pool.ExternalAsset.Symbol)
for _, mtp := range mtps {
BeginBlockerProcessMTP(ctx, k, mtp, pool)
}
}
}
}
}
func BeginBlockerProcessMTP(ctx sdk.Context, k Keeper, mtp *types.MTP, pool *clptypes.Pool) {
defer func() {
if r := recover(); r != nil {
if msg, ok := r.(string); ok {
ctx.Logger().Error(msg)
}
}
}()
h, err := k.UpdateMTPHealth(ctx, *mtp, *pool)
if err != nil {
return
}
mtp.MtpHealth = h
interestPayment = k.UpdateMTPInterestLiabilities(ctx, mtp, pool.InterestRate)
_ = k.IncrementalInterestPayment(ctx, interestPayment, mtp, pool) _ = k.SetMTP(ctx, mtp)
_, err = k.ForceCloseLong(ctx, &types.MsgForceClose{Id: mtp.Id, MtpAddress: mtp.Address})
if err == nil {
// Emit event if position was closed
k.EmitForceClose(ctx, mtp, "")
}
}
```
## Keepers
###### tags: `v1.0`
`Permissible Control Action Determination Prior`
Calls state update functions, simplisticly get and set, but more complex in implementation
Does a check on restriction on sender of message, for example, so incorporates Handler functionality
```go
type Keeper struct {
storeKey sdk.StoreKey
cdc codec.BinaryCodec
bankKeeper types.BankKeeper
clpKeeper types.CLPKeeper
adminKeeper adminkeeper.Keeper
paramStore paramtypes.Subspace
}
```
### Set MTP
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L60
```go
func (k Keeper) SetMTP(ctx sdk.Context, mtp *types.MTP) error {
store := ctx.KVStore(k.storeKey)
count := k.GetMTPCount(ctx)
if mtp.Id == 0 {
count++
mtp.Id = count
store.Set(types.MTPCountPrefix, types.GetIDBytes(count))
}
if err := mtp.Validate(); err != nil {
return err
}
key := types.GetMTPKey(mtp.Address, mtp.Id)
store.Set(key, k.cdc.MustMarshal(mtp))
return nil
}
```
### Get MTP Count
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L49
```go
func (k Keeper) GetMTPCount(ctx sdk.Context) uint64 {
var count uint64
countBz := ctx.KVStore(k.storeKey).Get(types.MTPCountPrefix)
if countBz == nil {
count = 0
} else {
count = types.GetIDFromBytes(countBz)
}
return count
}
```
### Get MTP Open Count
###### tags: `v1.0`
```go=
func (k Keeper) GetOpenMTPCount(ctx sdk.Context) uint64 {
var count uint64
countBz := ctx.KVStore(k.storeKey).Get(types.OpenMTPCountPrefix)
if countBz == nil {
count = 0
} else {
count = types.GetUint64FromBytes(countBz)
}
return count
}
```
### Get MTP
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L78
```go
func (k Keeper) GetMTP(ctx sdk.Context, mtpAddress string, id uint64) (types.MTP, error) {
var mtp types.MTP
key := types.GetMTPKey(mtpAddress, id)
store := ctx.KVStore(k.storeKey)
if !store.Has(key) {
return mtp, types.ErrMTPDoesNotExist
}
bz := store.Get(key)
k.cdc.MustUnmarshal(bz, &mtp)
return mtp, nil
}
```
### Get MTPs
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L101
```go
func (k Keeper) GetMTPs(ctx sdk.Context, pagination *query.PageRequest) ([]*types.MTP, *query.PageResponse, error) {
var mtpList []*types.MTP
store := ctx.KVStore(k.storeKey)
mtpStore := prefix.NewStore(store, types.MTPPrefix)
if pagination == nil {
pagination = &query.PageRequest{
Limit: math.MaxUint64 - 1,
}
}
pageRes, err := query.Paginate(mtpStore, pagination, func(key []byte, value []byte) error {
var mtp types.MTP
k.cdc.MustUnmarshal(value, &mtp)
mtpList = append(mtpList, &mtp)
return nil
})
return mtpList, pageRes, err
}
```
### Get MTP Iterator
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L90
```go
func (k Keeper) GetMTPIterator(ctx sdk.Context) sdk.Iterator {
store := ctx.KVStore(k.storeKey)
return sdk.KVStorePrefixIterator(store, types.MTPPrefix)
}
```
### Get MTPs For Address
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L160
```go
func (k Keeper) GetMTPsForAddress(ctx sdk.Context, mtpAddress sdk.Address, pagination *query.PageRequest) ([]*types.MTP, *query.PageResponse, error) {
var mtps []*types.MTP
store := ctx.KVStore(k.storeKey)
mtpStore := prefix.NewStore(store, types.GetMTPPrefixForAddress(mtpAddress.String()))
if pagination == nil {
pagination = &query.PageRequest{
Limit: MaxPageLimit,
}
}
if pagination.Limit > MaxPageLimit {
return nil, nil, status.Error(codes.InvalidArgument, fmt.Sprintf("page size greater than max %d", MaxPageLimit))
}
pageRes, err := query.Paginate(mtpStore, pagination, func(key []byte, value []byte) error {
var mtp types.MTP
k.cdc.MustUnmarshal(value, &mtp)
mtps = append(mtps, &mtp)
return nil
})
if err != nil {
return nil, nil, err
}
return mtps, pageRes, nil
}
```
### Get MTPs for Pool
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L139
```go=
func (k Keeper) GetMTPsForPool(ctx sdk.Context, asset string, pagination *query.PageRequest) ([]*types.MTP, *query.PageResponse, error) {
var mtps []*types.MTP
store := ctx.KVStore(k.storeKey)
mtpStore := prefix.NewStore(store, types.MTPPrefix)
if pagination == nil {
pagination = &query.PageRequest{
Limit: math.MaxUint64 - 1,
}
}
pageRes, err := query.FilteredPaginate(mtpStore, pagination, func(key []byte, value []byte, accumulate bool) (bool, error) {
var mtp types.MTP
k.cdc.MustUnmarshal(value, &mtp)
if accumulate && (strings.EqualFold(mtp.CustodyAsset, asset) || strings.EqualFold(mtp.CollateralAsset, asset)) {
mtps = append(mtps, &mtp)
return true, nil
}
return false, nil
})
return mtps, pageRes, err
}
```
### Destroy MTP
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/keeper.go#L168
```go
func (k Keeper) DestroyMTP(ctx sdk.Context, mtpAddress string, id uint64) error {
key := types.GetMTPKey(mtpAddress, id)
store := ctx.KVStore(k.storeKey)
if !store.Has(key) {
return types.ErrMTPDoesNotExist
}
store.Delete(key)
return nil
}
```
### GetSQBeginBlock
```go=
func (k Keeper) GetSQBeginBlock(ctx sdk.Context, pool *clptypes.Pool) uint64 {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.GetSQBeginBlockKey(pool))
if bz == nil {
return 0
}
return types.GetUint64FromBytes(bz)
}
```
### SetSQBeginBlock
```go
func (k Keeper) SetSQBeginBlock(ctx sdk.Context, pool *clptypes.Pool, height uint64) {
store := ctx.KVStore(k.storeKey)
store.Set(types.GetSQBeginBlockKey(pool), types.GetUint64Bytes(height))
}
```
### TrackSQBeginBlock
```go
func (k Keeper) TrackSQBeginBlock(ctx sdk.Context, pool *clptypes.Pool) {
threshold := k.GetRemovalQueueThreshold(ctx)
sqBeginBlock := k.GetSQBeginBlock(ctx, pool)
if sqBeginBlock == 0 {
if pool.Health.LTE(threshold) {
k.SetSQBeginBlock(ctx, pool, uint64(ctx.BlockHeight()))
k.EmitBelowRemovalThreshold(ctx, pool)
}
} else if pool.Health.GT(threshold) {
k.SetSQBeginBlock(ctx, pool, 0)
k.EmitAboveRemovalThreshold(ctx, pool)
}
}
```
### GetSQFromBlocks
```go
func (k Keeper) GetSQFromBlocks(ctx sdk.Context, pool clptypes.Pool, poolInterestRate sdk.Dec) sdk.Dec {
beginBlock := k.GetSQBeginBlock(ctx, &pool)
if beginBlock == 0 {
return sdk.ZeroDec()
}
blocks := ctx.BlockHeight() - int64(beginBlock)
maxInterestRate := k.GetInterestRateMax(ctx)
poolInterestRateFloat, _ := poolInterestRate.Float64()
minus := math.Pow(math.E, -1*poolInterestRateFloat*float64(blocks))
minusDec, err := sdk.NewDecFromStr(fmt.Sprintf("%v", minus))
if err != nil {
minusDec = sdk.NewDec(0)
}
multipliedBy := sdk.NewDec(1).Sub(minusDec)
return maxInterestRate.Mul(multipliedBy)
}
```
### Whitelist functions
```go=
func (k Keeper) IsWhitelisted(ctx sdk.Context, address string) bool {
store := ctx.KVStore(k.storeKey)
return store.Has(types.GetWhitelistKey(address))
}
func (k Keeper) WhitelistAddress(ctx sdk.Context, address string) {
store := ctx.KVStore(k.storeKey)
store.Set(types.GetWhitelistKey(address), []byte(address))
}
func (k Keeper) DewhitelistAddress(ctx sdk.Context, address string) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.GetWhitelistKey(address))
}
func (k Keeper) GetWhitelist(ctx sdk.Context, pagination *query.PageRequest) ([]string, *query.PageResponse, error) {
var list []string
store := ctx.KVStore(k.storeKey)
prefixStore := prefix.NewStore(store, types.WhitelistPrefix)
if pagination == nil {
pagination = &query.PageRequest{
Limit: math.MaxUint64 - 1,
}
}
pageRes, err := query.Paginate(prefixStore, pagination, func(key []byte, value []byte) error {
list = append(list, string(value))
return nil
})
return list, pageRes, err
}
```
### IsPoolClosed
```go=
func (k Keeper) IsPoolClosed(ctx sdk.Context, asset string) bool {
params := k.GetParams(ctx)
for _, p := range params.ClosedPools {
if types.StringCompare(p, asset) {
return true
}
}
return false
}
```
## Query
`Available Prescribed State Computation`
The end-user would want to get information on the liquidity pool and the margin trading system.
### Get MTP
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/grpc_query.go#L23
```go
func (srv queryServer) GetMTP(ctx context.Context, request *types.MTPRequest) (*types.MTPResponse, error) {
mtp, err := srv.keeper.GetMTP(sdk.UnwrapSDKContext(ctx), request.Address, request.Id)
if err != nil {
return nil, err
}
return &types.MTPResponse{Mtp: &mtp}, nil
}
```
### Get Positions For Address
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/grpc_query.go#L32
```go
func (srv queryServer) GetPositionsForAddress(ctx context.Context, request *types.PositionsForAddressRequest) (*types.PositionsForAddressResponse, error) {
addr, err := sdk.AccAddressFromBech32(request.Address)
if err != nil {
return nil, err
}
mtps, pageRes, err := srv.keeper.GetMTPsForAddress(sdk.UnwrapSDKContext(goCtx), addr, request.Pagination)
if err != nil {
return nil, err
}
return &types.PositionsForAddressResponse{Mtps: mtps, Pagination: pageRes}, nil
}
```
### Get Positions For Pool
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/grpc_query.go#L55
```go=
func (srv queryServer) GetPositionsByPool(ctx context.Context, request *types.PositionsByPoolRequest) (*types.PositionsByPoolResponse, error) {
mtps, pageRes, err := srv.keeper.GetMTPsForPool(sdk.UnwrapSDKContext(ctx), request.Asset, request.Pagination)
if err != nil {
return nil, err
}
return &types.PositionsByPoolResponse{
Mtps: mtps,
Pagination: pageRes,
}, nil
}
```
### Get Positions
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/grpc_query.go#L55
```go=
func (srv queryServer) GetPositions(ctx context.Context, request *types.PositionsRequest) (*types.PositionsResponse, error) {
if request.Pagination.Limit > MaxPageLimit {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("page size greater than max %d", MaxPageLimit))
}
mtps, page, err := srv.keeper.GetMTPs(sdk.UnwrapSDKContext(ctx), request.Pagination)
if err != nil {
return nil, err
}
return &types.PositionsResponse{
Mtps: mtps,
Pagination: page,
}, nil
}
```
### Get Status
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/grpc_query.go#L83
```go=
func (srv queryServer) GetStatus(ctx context.Context, request *types.StatusRequest) (*types.StatusResponse, error) {
return &types.StatusResponse{
OpenMtpCount: srv.keeper.GetOpenMTPCount(sdk.UnwrapSDKContext(ctx)),
LifetimeMtpCount: srv.keeper.GetMTPCount(sdk.UnwrapSDKContext(ctx)),
}, nil
}
```
### Get Params
###### tags: `v1.0`
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/keeper/grpc_query.go#L43
```go
func (srv queryServer) GetParams(ctx context.Context, request *types.ParamsRequest) (*types.ParamsResponse, error) {
params := srv.keeper.GetParams(sdk.UnwrapSDKContext(ctx))
return &types.ParamsResponse{Params: ¶ms}, nil
}
```
### Whitelist
```go=
func (srv queryServer) GetWhitelist(ctx context.Context, request *types.WhitelistRequest) (*types.WhitelistResponse, error) {
if request.Pagination.Limit > MaxPageLimit {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("page size greater than max %d", MaxPageLimit))
}
whitelist, page, err := srv.keeper.GetWhitelist(sdk.UnwrapSDKContext(ctx), request.Pagination)
if err != nil {
return nil, err
}
return &types.WhitelistResponse{
Whitelist: whitelist,
Pagination: page,
}, nil
}
```
## Querier
`Call on a Query`
Calls on a Query that an end-user could make are referenced here.
## Rest API
The legacy rest endpoints are documentated below. There is also a rest gateway autogenerated from the gRPC spec.
### Transactions
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/master/x/margin/client/rest/tx.go#L19
```go=
func registerTxRoutes(cliCtx client.Context, r *mux.Router) {
r.HandleFunc(
"/margin/open",
openHandler(cliCtx),
).Methods("POST")
r.HandleFunc(
"/margin/close",
closeHandler(cliCtx),
).Methods("POST")
r.HandleFunc(
"/margin/forceClose",
forceCloseHandler(cliCtx),
).Methods("POST")
}
type (
OpenReq struct {
BaseReq rest.BaseReq `json:"base_req"`
Signer string `json:"signer"` // User who is trying to open margin position
CollateralAsset clptypes.Asset `json:"collateral_asset"` // CollateralAsset for margin position
CollateralAmount sdk.Uint `json:"collateral_amount"` // CollateralAmount is the amount of collateral being added
BorrowAsset clptypes.Asset `json:"borrow_asset"` // BorrowAsset is asset being borrowed in margin position
Position types.Position `json:"position"` // Position type for margin position
}
CloseReq struct {
BaseReq rest.BaseReq `json:"base_req"`
Signer string `json:"signer"` // User who is trying to close margin position
// nolint:golint
Id uint64 `json:"id"` // Id of the mtp
}
ForceCloseReq struct {
BaseReq rest.BaseReq `json:"base_req"`
Signer string `json:"signer"` // User who is trying to close margin position
MtpAddress string `json:"mtp_address"` // MtpAddress for position to force close
// nolint:golint
Id uint64 `json:"id"` // Id of the mtp
}
)
```
### Queries
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/margin/client/rest/query.go#L19
```go=
func registerQueryRoutes(cliCtx client.Context, r *mux.Router) {
r.HandleFunc("/margin/mtps-by-address", getMTPsForAddress(cliCtx))
r.HandleFunc("/margin/params", getParams(cliCtx))
}
func getMTPsForAddress(cliCtx client.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
address, err := sdk.AccAddressFromBech32(r.URL.Query().Get("address"))
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
var limit, offset uint64
if r.URL.Query().Get("limit") != "" {
limit, err = strconv.ParseUint(r.URL.Query().Get("limit"), 10, 64)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
}
if r.URL.Query().Get("offset") != "" {
offset, err = strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
}
params := types.PositionsForAddressRequest{
Address: address.String(),
Pagination: &query.PageRequest{
Key: []byte(r.URL.Query().Get("key")),
Offset: offset,
Limit: limit,
CountTotal: false,
Reverse: false,
},
}
bz, err := cliCtx.LegacyAmino.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryMTPsForAddress)
res, height, err := cliCtx.QueryWithData(route, bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getParams(cliCtx client.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryParams)
res, height, err := cliCtx.Query(route)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
```
## Hooks
`Module Interface`
`Define outputs to CLP module`
`Changes to CLP module`
## CLP Message Server
### Remove Liquidity
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/clp/keeper/msg_server.go#L802
```go
func (srv msgServer) RemoveLiquidity(ctx context.Context, msg *types.MsgRemoveLiquidity) {
// Calculate remove amounts as normal except without mutating pool on swap.
withdrawNativeAssetAmount, withdrawExternalAssetAmount, lpUnitsLeft, swapAmount := CalculateWithdrawal(pool.PoolUnits,
pool.NativeAssetBalance.String(), pool.ExternalAssetBalance.String(), lp.LiquidityProviderUnits.String(),
msg.WBasisPoints.String(), msg.Asymmetry)
// Use the unbond requests before placing on queue or removing immediately.
err = k.Keeper.UseUnlockedLiquidity(ctx, lp,
lp.LiquidityProviderUnits.Sub(lpUnitsLeft), false)
if err != nil {
return nil, err
}
// ensure requested removal amount is less than available - what is already on the queue
lpQueuedUnits := srv.Keeper.GetRemovalQueueUnitsForLP(ctx, lp)
if msg.Units > lp.Units - lpQueuedUnits {
return nil, types.ErrInsufficientLiquidity
}
rowanValue = CalcWithdrawalRowanValue(pool, lp, msg...)
newPool := pool
newPool.NativeAssetBalance: pool.NativeAssetBalance - withdrawNativeAssetAmount
newPool.ExternalAssetBalance: pool.ExternalAssetBalance - withdrawExternalAssetAmount
if (marginKeeper.CalculatePoolHealth(newPool) < marginKeeper.GetParams().RemovalThreshold) {
srv.Keeper.QueueRemoval(ctx, msg, rowanValue)
return nil, types.ErrRemovalQueued
}
// continue as normal ....
}
```
Sifchain Implementation: https://github.com/Sifchain/sifnode/blob/feature/margin-1/x/clp/keeper/msg_server.go#L717
```go
func (srv msgServer) RemoveLiquidityUnits(ctx context.Context, msg *types.MsgRemoveLiquidity) {
// Calculate remove amounts as normal except without mutating pool on swap.
withdrawNativeAssetAmount, withdrawExternalAssetAmount, lpUnitsLeft := CalculateWithdrawalFromUnits(pool.PoolUnits,
pool.NativeAssetBalance.String(), pool.ExternalAssetBalance.String(), lp.LiquidityProviderUnits.String(),
msg.WithdrawUnits)
// ....
// Use the unbond requests before both placing on queue and removing immediately.
err = k.Keeper.UseUnlockedLiquidity(ctx, lp,
lp.LiquidityProviderUnits.Sub(lpUnitsLeft), false)
if err != nil {
return nil, err
}
// ensure requested removal amount is less than available - what is already on the queue
lpQueuedUnits := srv.Keeper.GetRemovalQueueUnitsForLP(ctx, lp)
if msg.Units > lp.Units - lpQueuedUnits {
return nil, types.ErrInsufficientLiquidity
}
rowanValue = CalcWithdrawalRowanValue(pool, lp, msg...)
newPool := pool
newPool.NativeAssetBalance: pool.NativeAssetBalance - removeNativeAssetAmount
newPool.ExternalAssetBalance: pool.ExternalAssetBalance - removeExternalAssetAmount
if (marginKeeper.CalculatePoolHealth(newPool) < marginKeeper.GetParams().RemovalThreshold) {
srv.Keeper.QueueRemoval(ctx,
types.MsgRemoveLiquidity{
Signer: msg.Signer,
ExternalAsset: msg.ExternalAsset,
WBasisPoints: ConvUnitsToWBasisPoints(lp.Units, msg.Units),
Asymmetry: 0,
}, rowanValue)
return nil, types.ErrRemovalQueued
}
// continue as normal ....
}
```
### Add Liquidity
```go=
func (srv msgServer) AddLiquidity(ctx context.Context, msg *types.AddLiquidityRequest) {
// Add liquidity as normal then process queue
newPoolUnits, lpUnits, err := CalculatePoolUnits(...)
if (srv.Keeper.RemovalQueueLen() > 0) {
srv.Keeper.ProcessRemovalQueue(ctx, msg, newPoolUnits)
}
}
```
## CLP Keeper
### ProcessRemovalQueue
###### tags: `v1.0`
```go=
func (k Keeper) ProcessRemovalQueue(ctx sdk.Context, msg *types.AddLiquidityRequest, unitsToDistribute sdk.Uint) {
perRequestUnits := unitsToDistribute / k.RemovalQueueLen()
for request := range k.GetRemovalQueue() {
if (request.Msg.ExternalAsset.Equals(msg.ExternalAsset)) {
pool := k.GetPool(ctx, msg.ExternalAsset.Symbol)
requestUnits := ConvWBasisToUnits(lp.Units, request.WBasis)
withdrawUnits := min(requestUnits, perRequestUnits)
withdrawWBasisPoints := ConvUnitsToWBasisPoints(lp.Units, withdrawUnits)
// Reuse removal logic using withdrawWBasisPoints
k.ProcessRemoveLiqiduityMsg(ctx, types.MsgRemoveLiquidity{
...
WBasisPoints: withdrawWBasisPoints
})
rowanValue = CalcWithdrawalRowanValue(lp.Units, withdrawWBasisPoints, ...)
// Update the queued request
k.SetProcessedRemovalRequest(ctx, request, withdrawWBasisPoints, rowanValue)
}
}
}
```