Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ import (
poatypes "github.com/peersyst/cbdc-node/x/poa/types"
"github.com/peersyst/cbdc-node/x/qbftclient"
qbfttypes "github.com/peersyst/cbdc-node/x/qbftclient/types"
"github.com/peersyst/cbdc-node/x/swap"
swapkeeper "github.com/peersyst/cbdc-node/x/swap/keeper"
swaptypes "github.com/peersyst/cbdc-node/x/swap/types"

srvflags "github.com/cosmos/evm/server/flags"

Expand Down Expand Up @@ -184,6 +187,9 @@ var (
feemarkettypes.ModuleName: nil,
poatypes.ModuleName: {authtypes.Minter, authtypes.Burner},
cbdctypes.ModuleName: {authtypes.Minter, authtypes.Burner},
// No permissions: x/swap mints nothing. Its module account holds only the
// transient escrow between a swap's two legs.
swaptypes.ModuleName: nil,
}
)

Expand Down Expand Up @@ -261,6 +267,7 @@ type App struct {
// cbdc keepers
PoaKeeper poakeeper.Keeper
CbdcKeeper cbdckeeper.Keeper
SwapKeeper swapkeeper.Keeper

// mm is the module manager
mm *module.Manager
Expand Down Expand Up @@ -315,6 +322,7 @@ func New(
// Ethermint
evmtypes.StoreKey, feemarkettypes.StoreKey,
erc20types.StoreKey,
swaptypes.StoreKey,
)
tkeys := storetypes.NewTransientStoreKeys(paramstypes.TStoreKey, evmtypes.TransientKey, feemarkettypes.TransientKey)

Expand Down Expand Up @@ -492,6 +500,13 @@ func New(
BaseDenom,
)

app.SwapKeeper = *swapkeeper.NewKeeper(
appCodec,
runtime.NewKVStoreService(keys[swaptypes.StoreKey]),
app.BankKeeper,
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)

// Ethermint keepers

// Feemarket Keeper
Expand Down Expand Up @@ -739,6 +754,7 @@ func New(
// cbdc app modules
poa.NewAppModule(appCodec, app.PoaKeeper, app.BankKeeper, app.StakingKeeper, app.AccountKeeper, app.interfaceRegistry),
cbdc.NewAppModule(appCodec, app.CbdcKeeper, app.AccountKeeper, app.interfaceRegistry),
swap.NewAppModule(appCodec, app.SwapKeeper, app.AccountKeeper),
)

// BasicModuleManager defines the module BasicManager which is in charge of setting up basic,
Expand Down Expand Up @@ -831,6 +847,7 @@ func New(
erc20types.ModuleName,
poatypes.ModuleName,
cbdctypes.ModuleName,
swaptypes.ModuleName,
crisistypes.ModuleName,
ratelimittypes.ModuleName,
}
Expand Down Expand Up @@ -887,6 +904,7 @@ func New(

app.setPostHandler()
app.setupUpgradeHandlers()
app.setupStoreLoader()

// At startup, after all modules have been registered, check that all prot
// annotations are correct.
Expand Down
56 changes: 56 additions & 0 deletions app/upgrades.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"context"
"errors"

storetypes "cosmossdk.io/store/types"
upgradetypes "cosmossdk.io/x/upgrade/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
appupgrades "github.com/peersyst/cbdc-node/app/upgrades"
swaptypes "github.com/peersyst/cbdc-node/x/swap/types"
)

// UpgradeNameIBCV2 is the on-chain upgrade name that activates IBC v2 (Eureka)
Expand All @@ -18,6 +20,22 @@ import (
// the v2-capable binary through the coordinated validator-vote upgrade flow.
const UpgradeNameIBCV2 = "ibc-v2"

// UpgradeNameSwap is the on-chain upgrade name that mounts x/swap.
//
// Unlike ibc-v2 this one DOES add a store key, and a new store cannot simply
// appear under a running chain: the root store refuses to load a store whose
// version is 0 while the rest are at the current height. The store has to be
// declared through StoreUpgrades so the upgrade module creates it at the halt
// height, which is what setupStoreLoader below arranges.
const UpgradeNameSwap = "swap"

// UpgradeNameSwapPairKey re-keys x/swap prices onto one entry per denom pair.
//
// No store key is added, so no StoreLoader is needed — only the keys inside the
// swap store move. It must still run: switching binary without it strands the v1
// entries at keys nothing looks up, where GetAllPrices still reports them.
const UpgradeNameSwapPairKey = "swap-pair-key"

var rollingUpgradeHandlers = []struct {
name string
handler upgradetypes.UpgradeHandler
Expand All @@ -39,6 +57,44 @@ func (app *App) setupUpgradeHandlers() {
return app.mm.RunMigrations(ctx, app.configurator, fromVM)
},
)

// x/swap's own InitGenesis does not run for a module added to a live chain,
// so RunMigrations is what registers it — and it comes up with the default
// params, i.e. no exchange address and conversion disabled. That is the
// intended landing state: the address is a group policy that has to be set
// deliberately afterwards, not guessed at upgrade time.
app.UpgradeKeeper.SetUpgradeHandler(
UpgradeNameSwap,
func(ctx context.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
return app.mm.RunMigrations(ctx, app.configurator, fromVM)
},
)

// RunMigrations sees x/swap at consensus version 1 and runs Migrate1to2.
app.UpgradeKeeper.SetUpgradeHandler(
UpgradeNameSwapPairKey,
func(ctx context.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
return app.mm.RunMigrations(ctx, app.configurator, fromVM)
},
)
}

// setupStoreLoader mounts stores added by a pending upgrade.
//
// Read from disk before the store is loaded, because by the time an upgrade
// handler runs the root store has already had to open every key — including the
// one the upgrade is introducing.
func (app *App) setupStoreLoader() {
upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk()
if err != nil {
panic(err)
}

if upgradeInfo.Name == UpgradeNameSwap && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) {
app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storetypes.StoreUpgrades{
Added: []string{swaptypes.StoreKey},
}))
}
}

func (app *App) setupPreBlockUpgradeHandlers(ctx sdk.Context) error {
Expand Down
20 changes: 20 additions & 0 deletions proto/swap/genesis.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
syntax = "proto3";
package swap;

import "gogoproto/gogo.proto";
import "swap/params.proto";
import "swap/swap.proto";

option go_package = "github.com/peersyst/cbdc-node/x/swap/types";

// GenesisState defines the swap module's genesis state.
message GenesisState {
// prices is the full set of stored exchange rates.
repeated Price prices = 1 [ (gogoproto.nullable) = false ];
// params is the module's parameter set.
Params params = 2 [ (gogoproto.nullable) = false ];
// pending_swaps are escrowed legs awaiting payout. Normally empty: an entry
// lives only between MsgSwap and MsgPaySwap, usually within one transaction.
// Exported so an escrow that outlived a restart is not silently dropped.
repeated PendingSwap pending_swaps = 3 [ (gogoproto.nullable) = false ];
}
21 changes: 21 additions & 0 deletions proto/swap/params.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
syntax = "proto3";
package swap;

import "cosmos_proto/cosmos.proto";

option go_package = "github.com/peersyst/cbdc-node/x/swap/types";

// Params defines the parameters for the module.
message Params {
// exchange_address receives the incoming asset when a swap converts it. It
// holds the collateral backing every unit this module has minted.
//
// It is gov-controlled via MsgUpdateParams rather than fixed at genesis
// because it is an x/group policy address, which does not exist until the
// group is created — after the chain is already running.
//
// Empty, the default, disables swapping entirely: with nowhere to put the
// incoming asset there is no way to convert without minting against
// collateral the chain never took custody of.
string exchange_address = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];
}
77 changes: 77 additions & 0 deletions proto/swap/query.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
syntax = "proto3";
package swap;

import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "swap/params.proto";
import "swap/swap.proto";

option go_package = "github.com/peersyst/cbdc-node/x/swap/types";

// Query defines the gRPC querier service.
service Query {
// Price queries the exchange rate stored for a single ordered denom pair.
//
// The denoms are query-string parameters rather than path segments because
// IBC voucher denoms contain a slash, which no path template can match.
rpc Price(QueryPriceRequest) returns (QueryPriceResponse) {
option (google.api.http).get = "/cbdc/swap/price";
}
// Prices queries every stored exchange rate.
rpc Prices(QueryPricesRequest) returns (QueryPricesResponse) {
option (google.api.http).get = "/cbdc/swap/prices";
}
// Params queries the module's parameter set.
rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
option (google.api.http).get = "/cbdc/swap/params";
}
// PendingSwap queries the escrowed-but-unpaid swap for an address, if any.
//
// The address is a query-string parameter for the same reason the price pair
// is: nothing about it is guaranteed path-safe.
rpc PendingSwap(QueryPendingSwapRequest) returns (QueryPendingSwapResponse) {
option (google.api.http).get = "/cbdc/swap/pending";
}
}

// QueryPendingSwapRequest is request type for the Query/PendingSwap RPC method.
message QueryPendingSwapRequest { string address = 1; }

// QueryPendingSwapResponse is response type for the Query/PendingSwap RPC method.
message QueryPendingSwapResponse {
// found is false when the address has nothing outstanding, which is the normal
// state — a pending swap exists only between an exchange's two legs.
bool found = 1;
PendingSwap pending_swap = 2 [ (gogoproto.nullable) = false ];
}

// QueryParamsRequest is request type for the Query/Params RPC method.
message QueryParamsRequest {}

// QueryParamsResponse is response type for the Query/Params RPC method.
message QueryParamsResponse {
Params params = 1 [ (gogoproto.nullable) = false ];
}

// QueryPriceRequest is request type for the Query/Price RPC method.
message QueryPriceRequest {
string from_denom = 1;
string to_denom = 2;
}

// QueryPriceResponse is response type for the Query/Price RPC method.
message QueryPriceResponse {
Price price = 1 [ (gogoproto.nullable) = false ];
}

// QueryPricesRequest is request type for the Query/Prices RPC method.
message QueryPricesRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;
}

// QueryPricesResponse is response type for the Query/Prices RPC method.
message QueryPricesResponse {
repeated Price prices = 1 [ (gogoproto.nullable) = false ];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
49 changes: 49 additions & 0 deletions proto/swap/swap.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
syntax = "proto3";
package swap;

import "gogoproto/gogo.proto";
import "cosmos_proto/cosmos.proto";
import "cosmos/base/v1beta1/coin.proto";

option go_package = "github.com/peersyst/cbdc-node/x/swap/types";

// Price is a one-directional exchange rate: how many units of to_denom one unit
// of from_denom is worth.
//
// A pair has exactly ONE entry, holding the direction it was quoted in. Both
// orderings are stored under the same key, so quoting from_denom -> to_denom
// replaces any to_denom -> from_denom rate rather than sitting beside it, and
// the opposite direction is derived as 1/price at settlement (see
// Keeper.GetEffectivePrice).
//
// Two independently stored directions would be a contradiction rather than two
// opinions: each leg would settle at its own number, and a pair quoted 0.402 one
// way and 50 the other turns a round trip into a 20x gain against the exchange.
// The cost of deriving instead is that the inverse carries no spread and its
// division truncates, always in the exchange's favour.
message Price {
string from_denom = 1;
string to_denom = 2;
string price = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
}

// PendingSwap is a swap whose incoming leg has been escrowed but whose payout
// has not happened yet.
//
// It exists because the two legs are authorised by two different group policies
// and cannot be carried by one message. The entry is written by MsgSwap and
// consumed by MsgPaySwap, normally inside the same transaction; MsgAssertSwapSettled
// exists so a transaction that failed to consume it cannot commit.
message PendingSwap {
// address is the account whose funds are escrowed, and the account the payout
// is owed to. One pending swap per address.
string address = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];
string from_denom = 2;
string to_denom = 3;
// escrowed is what left the address and sits in the module account.
cosmos.base.v1beta1.Coin escrowed = 4 [ (gogoproto.nullable) = false ];
}
Loading
Loading