From a4c5a1382274706430fb445856d67f30c87cd865 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Thu, 23 Apr 2020 13:57:25 -0700 Subject: [PATCH] [R4R] BEP3 module spec and clean up (#450) * bump SDK version to v0.38.2 * fix module.go and remove codespaces * fix coins Add() * fixes to handlers * migrate errors * more fixes * fixes fixes fixes * build * check for paramstore keytable * empty param validation function (TODO) * param validations * fix some tests * fix all tests * simulation fixes (WIP) * auction and bep3 sim refactor * fixes * bep3 sims fixes * auction and pricefeed fix * cdp sims fixes * fix tests * Update x/auction/keeper/auctions.go Co-Authored-By: Denali Marsh * Update x/bep3/types/params.go Co-Authored-By: Denali Marsh * Apply suggestions from code review Co-Authored-By: Denali Marsh * Update x/bep3/keeper/swap.go Co-Authored-By: Denali Marsh * address comments from review * address comments from review * fix: run sims * fix: implement marshal/unmarshal JSON for validator vesting account * fix: don't call set on sealed config * remove swap interface * add concepts spec * add state spec * add messages spec * update event names * implement swap expired event * add events spec * add params spec * add begin block spec * add module readme * update alias * revisions * aggregate expired swap ids for event emisison * markdown-link-check-disable for circleci * exclude api-endpoint links in Makefile Co-authored-by: Federico Kunze Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com> Co-authored-by: Kevin Davis --- Makefile | 2 +- x/auction/simulation/operations.go | 112 ++++++------------ x/bep3/alias.go | 4 +- x/bep3/keeper/swap.go | 59 +++++---- x/bep3/keeper/swap_test.go | 4 +- x/bep3/spec/01_concepts.md | 36 ++++++ x/bep3/spec/02_state.md | 82 +++++++++++++ x/bep3/spec/03_messages.md | 48 ++++++++ x/bep3/spec/04_events.md | 52 ++++++++ x/bep3/spec/05_params.md | 16 +++ x/bep3/spec/06_begin_block.md | 19 +++ x/bep3/spec/README.md | 24 ++-- .../diagrams/BEP3_binance_chain_to_kava.jpg | Bin 0 -> 30574 bytes .../diagrams/BEP3_kava_to_binance_chain.jpg | Bin 0 -> 31192 bytes x/bep3/types/codec.go | 2 - x/bep3/types/common_test.go | 2 +- x/bep3/types/events.go | 26 ++-- x/bep3/types/swap.go | 12 +- x/bep3/types/swap_test.go | 2 +- 19 files changed, 357 insertions(+), 145 deletions(-) create mode 100644 x/bep3/spec/01_concepts.md create mode 100644 x/bep3/spec/02_state.md create mode 100644 x/bep3/spec/03_messages.md create mode 100644 x/bep3/spec/04_events.md create mode 100644 x/bep3/spec/05_params.md create mode 100644 x/bep3/spec/06_begin_block.md create mode 100644 x/bep3/spec/diagrams/BEP3_binance_chain_to_kava.jpg create mode 100644 x/bep3/spec/diagrams/BEP3_kava_to_binance_chain.jpg diff --git a/Makefile b/Makefile index ec0884fa..7becb364 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,7 @@ clean: # This tool checks local markdown links as well. # Set to exclude riot links as they trigger false positives link-check: - @go run github.com/raviqqe/liche -r . --exclude "^http://127.*|^https://riot.im/app*" + @go run github.com/raviqqe/liche -r . --exclude "^http://127.*|^https://riot.im/app*|^http://kava-testnet*|^https://testnet-dex*" ######################################## ### Testing diff --git a/x/auction/simulation/operations.go b/x/auction/simulation/operations.go index 84f2297c..503c78b9 100644 --- a/x/auction/simulation/operations.go +++ b/x/auction/simulation/operations.go @@ -21,8 +21,8 @@ import ( ) var ( - errorNotEnoughCoins = errors.New("account doesn't have enough coins") - errorCantReceiveBids = errors.New("auction can't receive bids (lot = 0 in reverse auction)") + noOpMsg = simulation.NoOpMsg(types.ModuleName) + ErrorNotEnoughCoins = errors.New("account doesn't have enough coins") ) // Simulation operation weights constants @@ -72,11 +72,10 @@ func SimulateMsgPlaceBid(ak auth.AccountKeeper, keeper keeper.Keeper) simulation // search through auctions and an accounts to find a pair where a bid can be placed (ie account has enough coins to place bid on auction) blockTime := ctx.BlockHeader().Time - params := keeper.GetParams(ctx) bidder, openAuction, found := findValidAccountAuctionPair(accs, openAuctions, func(acc simulation.Account, auc types.Auction) bool { account := ak.GetAccount(ctx, acc.Address) - _, err := generateBidAmount(r, params, auc, account, blockTime) - if err == errorNotEnoughCoins || err == errorCantReceiveBids { + _, err := generateBidAmount(r, auc, account, blockTime) + if err == ErrorNotEnoughCoins { return false // keep searching } else if err != nil { panic(err) // raise errors @@ -89,21 +88,27 @@ func SimulateMsgPlaceBid(ak auth.AccountKeeper, keeper keeper.Keeper) simulation bidderAcc := ak.GetAccount(ctx, bidder.Address) if bidderAcc == nil { - return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("couldn't find account %s", bidder.Address) + return simulation.NoOpMsg(types.ModuleName), nil, nil } // pick a bid amount for the chosen auction and bidder - amount, err := generateBidAmount(r, params, openAuction, bidderAcc, blockTime) - if err != nil { // shouldn't happen given the checks above + amount, err := generateBidAmount(r, openAuction, bidderAcc, blockTime) + if err != nil { return simulation.NoOpMsg(types.ModuleName), nil, err } - // create and deliver a tx + // create a msg msg := types.NewMsgPlaceBid(openAuction.GetID(), bidder.Address, amount) + spendable := bidderAcc.SpendableCoins(ctx.BlockTime()) + fees, err := simulation.RandomFees(r, ctx, spendable) + if err != nil { + return simulation.NoOpMsg(types.ModuleName), nil, err + } + tx := helpers.GenTx( []sdk.Msg{msg}, - sdk.NewCoins(), // TODO pick a random amount fees + fees, helpers.DefaultGenTxGas, chainID, []uint64{bidderAcc.GetAccountNumber()}, @@ -113,103 +118,60 @@ func SimulateMsgPlaceBid(ak auth.AccountKeeper, keeper keeper.Keeper) simulation _, result, err := app.Deliver(tx) if err != nil { - // to aid debugging, add the stack trace to the comment field of the returned opMsg - return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err + return simulation.NoOpMsg(types.ModuleName), nil, err } - // to aid debugging, add the result log to the comment field + + // Return an operationMsg indicating whether the msg was submitted successfully + // Using result.Log as the comment field as it contains any error message emitted by the keeper return simulation.NewOperationMsg(msg, true, result.Log), nil, nil } } -func generateBidAmount( - r *rand.Rand, params types.Params, auc types.Auction, - bidder authexported.Account, blockTime time.Time) (sdk.Coin, error) { +func generateBidAmount(r *rand.Rand, auc types.Auction, bidder authexported.Account, blockTime time.Time) (sdk.Coin, error) { bidderBalance := bidder.SpendableCoins(blockTime) switch a := auc.(type) { case types.DebtAuction: - // Check bidder has enough (stable coin) to pay in if bidderBalance.AmountOf(a.Bid.Denom).LT(a.Bid.Amount) { // stable coin - return sdk.Coin{}, errorNotEnoughCoins + return sdk.Coin{}, ErrorNotEnoughCoins } - // Check auction can still receive new bids - if a.Lot.Amount.Equal(sdk.ZeroInt()) { - return sdk.Coin{}, errorCantReceiveBids - } - // Generate a new lot amount (gov coin) - maxNewLotAmt := a.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost - sdk.MaxInt( - sdk.NewInt(1), - sdk.NewDecFromInt(a.Lot.Amount).Mul(params.IncrementDebt).RoundInt(), - ), - ) - amt, err := RandIntInclusive(r, sdk.ZeroInt(), maxNewLotAmt) // maxNewLotAmt shouldn't be < 0 given the check above + amt, err := RandIntInclusive(r, sdk.ZeroInt(), a.Lot.Amount) // pick amount less than current lot amount // TODO min bid increments if err != nil { panic(err) } return sdk.NewCoin(a.Lot.Denom, amt), nil // gov coin case types.SurplusAuction: - // Check the bidder has enough (gov coin) to pay in - minNewBidAmt := a.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost - sdk.MaxInt( - sdk.NewInt(1), - sdk.NewDecFromInt(a.Bid.Amount).Mul(params.IncrementSurplus).RoundInt(), - ), - ) - if bidderBalance.AmountOf(a.Bid.Denom).LT(minNewBidAmt) { // gov coin - return sdk.Coin{}, errorNotEnoughCoins + if bidderBalance.AmountOf(a.Bid.Denom).LT(a.Bid.Amount) { // gov coin // TODO account for bid increments + return sdk.Coin{}, ErrorNotEnoughCoins } - // Generate a new bid amount (gov coin) - amt, err := RandIntInclusive(r, minNewBidAmt, bidderBalance.AmountOf(a.Bid.Denom)) + amt, err := RandIntInclusive(r, a.Bid.Amount, bidderBalance.AmountOf(a.Bid.Denom)) if err != nil { panic(err) } return sdk.NewCoin(a.Bid.Denom, amt), nil // gov coin case types.CollateralAuction: - // Check the bidder has enough (stable coin) to pay in - minNewBidAmt := a.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost - sdk.MaxInt( - sdk.NewInt(1), - sdk.NewDecFromInt(a.Bid.Amount).Mul(params.IncrementCollateral).RoundInt(), - ), - ) - minNewBidAmt = sdk.MinInt(minNewBidAmt, a.MaxBid.Amount) // allow new bids to hit MaxBid even though it may be less than the increment % - if bidderBalance.AmountOf(a.Bid.Denom).LT(minNewBidAmt) { - return sdk.Coin{}, errorNotEnoughCoins + if bidderBalance.AmountOf(a.Bid.Denom).LT(a.Bid.Amount) { // stable coin // TODO account for bid increments (in forward phase) + return sdk.Coin{}, ErrorNotEnoughCoins } - // Check auction can still receive new bids - if a.IsReversePhase() && a.Lot.Amount.Equal(sdk.ZeroInt()) { - return sdk.Coin{}, errorCantReceiveBids - } - // Generate a new bid amount (collateral coin in reverse phase) if a.IsReversePhase() { - maxNewLotAmt := a.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost - sdk.MaxInt( - sdk.NewInt(1), - sdk.NewDecFromInt(a.Lot.Amount).Mul(params.IncrementCollateral).RoundInt(), - ), - ) - amt, err := RandIntInclusive(r, sdk.ZeroInt(), maxNewLotAmt) // maxNewLotAmt shouldn't be < 0 given the check above + amt, err := RandIntInclusive(r, sdk.ZeroInt(), a.Lot.Amount) // pick amount less than current lot amount if err != nil { panic(err) } return sdk.NewCoin(a.Lot.Denom, amt), nil // collateral coin - - // Generate a new bid amount (stable coin in forward phase) - } else { - amt, err := RandIntInclusive(r, minNewBidAmt, sdk.MinInt(bidderBalance.AmountOf(a.Bid.Denom), a.MaxBid.Amount)) - if err != nil { - panic(err) - } - // when the bidder has enough coins, pick the MaxBid amount more frequently to increase chance auctions phase get into reverse phase - if r.Intn(2) == 0 && bidderBalance.AmountOf(a.Bid.Denom).GTE(a.MaxBid.Amount) { // 50% - amt = a.MaxBid.Amount - } - return sdk.NewCoin(a.Bid.Denom, amt), nil // stable coin } + amt, err := RandIntInclusive(r, a.Bid.Amount, sdk.MinInt(bidderBalance.AmountOf(a.Bid.Denom), a.MaxBid.Amount)) + if err != nil { + panic(err) + } + // pick the MaxBid amount more frequently to increase chance auctions phase get into reverse phase + if r.Intn(10) == 0 { // 10% + amt = a.MaxBid.Amount + } + return sdk.NewCoin(a.Bid.Denom, amt), nil // stable coin default: return sdk.Coin{}, fmt.Errorf("unknown auction type") diff --git a/x/bep3/alias.go b/x/bep3/alias.go index 81097b2c..1a315ad9 100644 --- a/x/bep3/alias.go +++ b/x/bep3/alias.go @@ -10,6 +10,7 @@ const ( AddrByteCount = types.AddrByteCount AttributeKeyAmount = types.AttributeKeyAmount AttributeKeyAtomicSwapID = types.AttributeKeyAtomicSwapID + AttributeKeyAtomicSwapIDs = types.AttributeKeyAtomicSwapIDs AttributeKeyClaimSender = types.AttributeKeyClaimSender AttributeKeyDirection = types.AttributeKeyDirection AttributeKeyExpectedIncome = types.AttributeKeyExpectedIncome @@ -31,8 +32,8 @@ const ( DepositAtomicSwap = types.DepositAtomicSwap EventTypeClaimAtomicSwap = types.EventTypeClaimAtomicSwap EventTypeCreateAtomicSwap = types.EventTypeCreateAtomicSwap - EventTypeDepositAtomicSwap = types.EventTypeDepositAtomicSwap EventTypeRefundAtomicSwap = types.EventTypeRefundAtomicSwap + EventTypeSwapsExpired = types.EventTypeSwapsExpired Expired = types.Expired INVALID = types.INVALID Incoming = types.Incoming @@ -135,7 +136,6 @@ type ( QueryAtomicSwapByID = types.QueryAtomicSwapByID QueryAtomicSwaps = types.QueryAtomicSwaps SupplyKeeper = types.SupplyKeeper - Swap = types.Swap SwapDirection = types.SwapDirection SwapStatus = types.SwapStatus ) diff --git a/x/bep3/keeper/swap.go b/x/bep3/keeper/swap.go index 21fd2e08..58d53121 100644 --- a/x/bep3/keeper/swap.go +++ b/x/bep3/keeper/swap.go @@ -77,6 +77,9 @@ func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, times timestamp, sender, recipient, senderOtherChain, recipientOtherChain, 0, types.Open, crossChain, direction) + k.SetAtomicSwap(ctx, atomicSwap) + k.InsertIntoByBlockIndex(ctx, atomicSwap) + // Emit 'create_atomic_swap' event ctx.EventManager().EmitEvent( sdk.NewEvent( @@ -94,8 +97,6 @@ func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, times ), ) - k.SetAtomicSwap(ctx, atomicSwap) - k.InsertIntoByBlockIndex(ctx, atomicSwap) return nil } @@ -103,7 +104,7 @@ func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, times func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []byte, randomNumber []byte) error { atomicSwap, found := k.GetAtomicSwap(ctx, swapID) if !found { - return sdkerrors.Wrapf(types.ErrAtomicSwapNotFound, "%d", swapID) + return sdkerrors.Wrapf(types.ErrAtomicSwapNotFound, "%s", swapID) } // Only open atomic swaps can be claimed @@ -148,6 +149,15 @@ func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []b return err } + // Complete swap + atomicSwap.Status = types.Completed + atomicSwap.ClosedBlock = ctx.BlockHeight() + k.SetAtomicSwap(ctx, atomicSwap) + + // Remove from byBlock index and transition to longterm storage + k.RemoveFromByBlockIndex(ctx, atomicSwap) + k.InsertIntoLongtermStorage(ctx, atomicSwap) + // Emit 'claim_atomic_swap' event ctx.EventManager().EmitEvent( sdk.NewEvent( @@ -160,14 +170,6 @@ func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []b ), ) - // Complete swap - atomicSwap.Status = types.Completed - atomicSwap.ClosedBlock = ctx.BlockHeight() - k.SetAtomicSwap(ctx, atomicSwap) - - // Remove from byBlock index and transition to longterm storage - k.RemoveFromByBlockIndex(ctx, atomicSwap) - k.InsertIntoLongtermStorage(ctx, atomicSwap) return nil } @@ -202,6 +204,14 @@ func (k Keeper) RefundAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID [] return err } + // Complete swap + atomicSwap.Status = types.Completed + atomicSwap.ClosedBlock = ctx.BlockHeight() + k.SetAtomicSwap(ctx, atomicSwap) + + // Transition to longterm storage + k.InsertIntoLongtermStorage(ctx, atomicSwap) + // Emit 'refund_atomic_swap' event ctx.EventManager().EmitEvent( sdk.NewEvent( @@ -213,13 +223,6 @@ func (k Keeper) RefundAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID [] ), ) - // Complete swap - atomicSwap.Status = types.Completed - atomicSwap.ClosedBlock = ctx.BlockHeight() - k.SetAtomicSwap(ctx, atomicSwap) - - // Transition to longterm storage - k.InsertIntoLongtermStorage(ctx, atomicSwap) return nil } @@ -232,12 +235,24 @@ func (k Keeper) UpdateExpiredAtomicSwaps(ctx sdk.Context) error { }) // Expire incomplete swaps (claimed swaps have already been removed from byBlock index) + var expiredSwapIDs []string for _, id := range expiredSwaps { - swap, _ := k.GetAtomicSwap(ctx, id) - swap.Status = types.Expired - k.SetAtomicSwap(ctx, swap) - k.RemoveFromByBlockIndex(ctx, swap) + atomicSwap, _ := k.GetAtomicSwap(ctx, id) + atomicSwap.Status = types.Expired + k.SetAtomicSwap(ctx, atomicSwap) + k.RemoveFromByBlockIndex(ctx, atomicSwap) + expiredSwapIDs = append(expiredSwapIDs, hex.EncodeToString(atomicSwap.GetSwapID())) } + + // Emit 'swaps_expired' event + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeSwapsExpired, + sdk.NewAttribute(types.AttributeKeyAtomicSwapIDs, fmt.Sprintf("%s", expiredSwapIDs)), + sdk.NewAttribute(types.AttributeExpirationBlock, fmt.Sprintf("%d", ctx.BlockHeight())), + ), + ) + return nil } diff --git a/x/bep3/keeper/swap_test.go b/x/bep3/keeper/swap_test.go index 1440e3e2..3389cf58 100644 --- a/x/bep3/keeper/swap_test.go +++ b/x/bep3/keeper/swap_test.go @@ -337,7 +337,7 @@ func (suite *AtomicSwapTestSuite) TestCreateAtomicSwap() { suite.NotNil(actualSwap) // Confirm swap contents - expectedSwap := types.Swap( + expectedSwap := types.AtomicSwap{ Amount: tc.args.coins, RandomNumberHash: tc.args.randomNumberHash, @@ -351,7 +351,7 @@ func (suite *AtomicSwapTestSuite) TestCreateAtomicSwap() { Status: types.Open, CrossChain: tc.args.crossChain, Direction: tc.args.direction, - }) + } suite.Equal(expectedSwap, actualSwap) } else { suite.Error(err) diff --git a/x/bep3/spec/01_concepts.md b/x/bep3/spec/01_concepts.md new file mode 100644 index 00000000..f897237c --- /dev/null +++ b/x/bep3/spec/01_concepts.md @@ -0,0 +1,36 @@ +# Concepts + + The BEP3 module implements the [BEP3 protocol](https://github.com/binance-chain/BEPs/blob/master/BEP3.md) for secure cross-chain asset transfers between Kava and other BEP3 compatible chains, such as Binance Chain. Tranactions are witnessed and relayed between the two blockchains by Binance's BEP3 deputy process. The deputy maintains an address on both chains and is responsible for delivering tokens upon the successful completion of an Atomic Swap. Learn more about the BEP3 deputy process [here](https://github.com/binance-chain/bep3-deputy). + +## Requirements +Kava +- The deputy’s Kava testnet-5000 address is **kava1aphsdnz5hu2t5ty2au6znprug5kx3zpy6zwq29**. +- We recommend using http://kava-testnet-5000.kava.io:1317 as Kava’s API endpoint. + +Binance Chain +- The deputy’s Binance Chain testnet address is **tbnb1et8vmd0dgvswjnyaf73ez8ye0jehc8a7t7fljv**. +- We recommend using https://testnet-dex.binance.org/ as Binance Chain’s API endpoint. + +Kava's [JavaScript SDK](https://github.com/Kava-Labs/javascript-sdk) and Binance Chain’s [JavaScript SDK](https://github.com/binance-chain/javascript-sdk) can be used to create, claim, and refund swaps. + +## Binance Chain to Kava + +When a user wants to transfer tokens from Binance Chain to Kava, the following steps are taken: +1. User’s tokens are locked on Binance Chain along with the hash of a secret only known to the user. If the secret is not revealed before the deadline, the tokens are refundable. +2. The deputy sends a message to Kava saying “a user has locked X tokens, if their secret is revealed before the deadline issue them an equivalent amount of pegged tokens”. +3. The user reveals the secret on Kava and receives the pegged tokens. +4. The deputy relays the secret to Binance Chain and the original tokens are locked permanently. + + +![Binance Chain to Kava Diagram](./diagrams/BEP3_binance_chain_to_kava.jpg) + +## Kava to Binance Chain +1. When a user wants to transfer tokens from Kava to Binance Chain by redeeming pegged tokens, the following steps are taken: +User’s pegged tokens are locked on Kava along with the hash of a secret only known to the user. If the secret is not revealed before the deadline, the tokens are refundable. +2. The deputy sends a message to Binance Chain saying “a user has locked X pegged tokens, if their secret is revealed before the deadline issue them an equivalent amount of tokens”. +3. The user reveals the secret on Binance Chain and receives the tokens. +4. The deputy relays the secret to Kava and the pegged tokens are locked permanently. + + +![Kava to Binance Chain Diagram](./diagrams/BEP3_kava_to_binance_chain.jpg) + diff --git a/x/bep3/spec/02_state.md b/x/bep3/spec/02_state.md new file mode 100644 index 00000000..7ce2df13 --- /dev/null +++ b/x/bep3/spec/02_state.md @@ -0,0 +1,82 @@ +# State + +## Parameters and genesis state + +`Paramaters` define the rules according to which swaps are executed. Parameter updates can be made via on-chain parameter update proposals. + +```go +// Params governance parameters for bep3 module +type Params struct { + BnbDeputyAddress sdk.AccAddress `json:"bnb_deputy_address" yaml:"bnb_deputy_address"` // deputy's address on Kava + MinBlockLock int64 `json:"min_block_lock" yaml:"min_block_lock"` // minimum swap expire height + MaxBlockLock int64 `json:"max_block_lock" yaml:"max_block_lock"` // maximum swap expire height + SupportedAssets AssetParams `json:"supported_assets" yaml:"supported_assets"` // array of supported asset +} + +// AssetParam governance parameters for each asset within a supported chain +type AssetParam struct { + Denom string `json:"denom" yaml:"denom"` // name of the asset + CoinID int `json:"coin_id" yaml:"coin_id"` // internationally recognized coin ID + Limit sdk.Int `json:"limit" yaml:"limit"` // asset supply limit + Active bool `json:"active" yaml:"active"` // denotes if asset is active or paused +} +``` + +`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the bep3 module to resume. + +```go +// GenesisState - all bep3 state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params" yaml:"params"` + AtomicSwaps AtomicSwaps `json:"atomic_swaps" yaml:"atomic_swaps"` + AssetSupplies AssetSupplies `json:"assets_supplies" yaml:"assets_supplies"` +} +``` + +## Types + +```go +// AtomicSwap contains the information for an atomic swap +type AtomicSwap struct { + Amount sdk.Coins `json:"amount" yaml:"amount"` + RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"` + ExpireHeight int64 `json:"expire_height" yaml:"expire_height"` + Timestamp int64 `json:"timestamp" yaml:"timestamp"` + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Recipient sdk.AccAddress `json:"recipient" yaml:"recipient"` + SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"` + RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"` + ClosedBlock int64 `json:"closed_block" yaml:"closed_block"` + Status SwapStatus `json:"status" yaml:"status"` + CrossChain bool `json:"cross_chain" yaml:"cross_chain"` + Direction SwapDirection `json:"direction" yaml:"direction"` +} + +// SwapStatus is the status of an AtomicSwap +type SwapStatus byte + +const ( + NULL SwapStatus = 0x00 + Open SwapStatus = 0x01 + Completed SwapStatus = 0x02 + Expired SwapStatus = 0x03 +) + +// SwapDirection is the direction of an AtomicSwap +type SwapDirection byte + +const ( + INVALID SwapDirection = 0x00 + Incoming SwapDirection = 0x01 + Outgoing SwapDirection = 0x02 +) + +// AssetSupply contains information about an asset's supply +type AssetSupply struct { + Denom string `json:"denom" yaml:"denom"` + IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"` + OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"` + CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"` + Limit sdk.Coin `json:"limit" yaml:"limit"` +} +``` \ No newline at end of file diff --git a/x/bep3/spec/03_messages.md b/x/bep3/spec/03_messages.md new file mode 100644 index 00000000..97ef3912 --- /dev/null +++ b/x/bep3/spec/03_messages.md @@ -0,0 +1,48 @@ +# Messages + +## Create swap + +Swaps are created using the `MsgCreateAtomicSwap` message type. + +```go +// MsgCreateAtomicSwap contains an AtomicSwap struct +type MsgCreateAtomicSwap struct { + From sdk.AccAddress `json:"from" yaml:"from"` + To sdk.AccAddress `json:"to" yaml:"to"` + RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"` + SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"` + RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"` + Timestamp int64 `json:"timestamp" yaml:"timestamp"` + Amount sdk.Coins `json:"amount" yaml:"amount"` + ExpectedIncome string `json:"expected_income" yaml:"expected_income"` + HeightSpan int64 `json:"height_span" yaml:"height_span"` + CrossChain bool `json:"cross_chain" yaml:"cross_chain"` +} +``` + +## Claim swap + +Active swaps are claimed using the `MsgClaimAtomicSwap` message type. + +```go +// MsgClaimAtomicSwap defines a AtomicSwap claim +type MsgClaimAtomicSwap struct { + From sdk.AccAddress `json:"from" yaml:"from"` + SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"` + RandomNumber tmbytes.HexBytes `json:"random_number" yaml:"random_number"` +} +``` + + + +## Refund swap + +Expired swaps are refunded using the `MsgRefundAtomicSwap` message type. + +```go +// MsgRefundAtomicSwap defines a refund msg +type MsgRefundAtomicSwap struct { + From sdk.AccAddress `json:"from" yaml:"from"` + SwapID tmbytes.HexBytes `json:"swap_id" yaml:"swap_id"` +} +``` \ No newline at end of file diff --git a/x/bep3/spec/04_events.md b/x/bep3/spec/04_events.md new file mode 100644 index 00000000..059a2055 --- /dev/null +++ b/x/bep3/spec/04_events.md @@ -0,0 +1,52 @@ +# Events + +The `x/bep3` module emits the following events: + +## Handlers + +### MsgCreateAtomicSwap + +| Type | Attribute Key | Attribute Value | +|--------------------|--------------------|--------------------------| +| create_atomic_swap | sender | {sender address} | +| create_atomic_swap | recipient | {recipient address} | +| create_atomic_swap | atomic_swap_id | {swap ID} | +| create_atomic_swap | random_number_hash | {random number hash} | +| create_atomic_swap | timestamp | {timestamp} | +| create_atomic_swap | sender_other_chain | {sender other chain} | +| create_atomic_swap | expire_height | {swap expiration block} | +| create_atomic_swap | amount | {coin amount} | +| create_atomic_swap | expected_income | {expected value received}| +| create_atomic_swap | direction | {incoming or outgoing} | +| message | module | bep3 | +| message | sender | {sender address} | + +### MsgClaimAtomicSwap + +| Type | Attribute Key | Attribute Value | +|--------------------|--------------------|--------------------------| +| claim_atomic_swap | claim_sender | {sender address} | +| claim_atomic_swap | recipient | {recipient address} | +| claim_atomic_swap | atomic_swap_id | {swap ID} | +| claim_atomic_swap | random_number_hash | {random number hash} | +| claim_atomic_swap | random_number | {secret random number} | +| message | module | bep3 | +| message | sender | {sender address} | + +## MsgRefundAtomicSwap + +| Type | Attribute Key | Attribute Value | +|--------------------|--------------------|--------------------------| +| refund_atomic_swap | refund_sender | {sender address} | +| refund_atomic_swap | sender | {swap creator address} | +| refund_atomic_swap | atomic_swap_id | {swap ID} | +| refund_atomic_swap | random_number_hash | {random number hash} | +| message | module | bep3 | +| message | sender | {sender address} | + +## BeginBlock + +| Type | Attribute Key | Attribute Value | +|---------------|------------------|------------------------------| +| swaps_expired | atomic_swap_ids | {array of swap IDs} | +| swaps_expired | expiration_block | {block height at expiration} | diff --git a/x/bep3/spec/05_params.md b/x/bep3/spec/05_params.md new file mode 100644 index 00000000..bcb0b51f --- /dev/null +++ b/x/bep3/spec/05_params.md @@ -0,0 +1,16 @@ +# Parameters + +The bep3 module contains the following parameters: + +| Key | Type | Example | Description | +|-------------------|-------------------------|-----------------------------------------------|-------------------------------| +| BnbDeputyAddress | string (sdk.AccAddress) | "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj" | deputy's Kava address | +| MinBlockLock | int64 | 80 | minimum swap expire height | +| MaxBlockLock | int64 | 600 | maximum swap expire height | +| SupportedAssets | AssetParams | []AssetParam | array of supported assets | +|-------------------|-------------------------|-----------------------------------------------|-------------------------------| +| AssetParam | AssetParam | AssetParam{"bnb", 714, sdk.NewInt(100), true} | a supported asset | +| AssetParam.Denom | string | "bnb" | asset's name | +| AssetParam.CoinID | int64 | 714 | asset's international coin ID | +| AssetParam.Limit | sdk.Int | sdk.NewInt(100) | asset's supply limit | +| AssetParam.Active | boolean | true | asset's state: live or paused | diff --git a/x/bep3/spec/06_begin_block.md b/x/bep3/spec/06_begin_block.md new file mode 100644 index 00000000..44a8f328 --- /dev/null +++ b/x/bep3/spec/06_begin_block.md @@ -0,0 +1,19 @@ +# Begin Block + +At the start of each block, atomic swaps that have reached `ExpireHeight` are expired. The logic to expire atomic swaps is as follows: + +```go + var expiredSwaps [][]byte + k.IterateAtomicSwapsByBlock(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool { + expiredSwaps = append(expiredSwaps, id) + return false + }) + + // Expire incomplete swaps (claimed swaps have already been removed from byBlock index) + for _, id := range expiredSwaps { + atomicSwap, _ := k.GetAtomicSwap(ctx, id) + atomicSwap.Status = types.Expired + k.SetAtomicSwap(ctx, atomicSwap) + k.RemoveFromByBlockIndex(ctx, atomicSwap) + } +``` diff --git a/x/bep3/spec/README.md b/x/bep3/spec/README.md index 86472c50..226e2c66 100644 --- a/x/bep3/spec/README.md +++ b/x/bep3/spec/README.md @@ -1,17 +1,13 @@ -# bep3 module specification +# `bep3` module specification + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** +5. **[Params](05_params.md)** +6. **[BeginBlock](06_begin_block.md)** ## Abstract - - -## Contents - -// TODO: Create the below files if they are needed. - +`x/bep3` is an implementation of a Cosmos SDK Module that handles cross-chain Atomic Swaps between Kava and blockchains that implement the BEP3 protocol. Atomic Swaps are created, then either claimed before their expiration block or refunded after they've expired. diff --git a/x/bep3/spec/diagrams/BEP3_binance_chain_to_kava.jpg b/x/bep3/spec/diagrams/BEP3_binance_chain_to_kava.jpg new file mode 100644 index 0000000000000000000000000000000000000000..350d659a916042d9be98e27ca323eeff7bc14173 GIT binary patch literal 30574 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
52VT?L)hv^w5v@& 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 literal 0 HcmV?d00001 diff --git a/x/bep3/types/codec.go b/x/bep3/types/codec.go index ec9f2229..8ee32588 100644 --- a/x/bep3/types/codec.go +++ b/x/bep3/types/codec.go @@ -12,8 +12,6 @@ func init() { // RegisterCodec registers concrete types on amino func RegisterCodec(cdc *codec.Codec) { - cdc.RegisterInterface((*Swap)(nil), nil) - cdc.RegisterConcrete(MsgCreateAtomicSwap{}, "bep3/MsgCreateAtomicSwap", nil) cdc.RegisterConcrete(MsgRefundAtomicSwap{}, "bep3/MsgRefundAtomicSwap", nil) cdc.RegisterConcrete(MsgClaimAtomicSwap{}, "bep3/MsgClaimAtomicSwap", nil) diff --git a/x/bep3/types/common_test.go b/x/bep3/types/common_test.go index 11fd749e..57601d1e 100644 --- a/x/bep3/types/common_test.go +++ b/x/bep3/types/common_test.go @@ -29,7 +29,7 @@ func atomicSwap(index int) types.AtomicSwap { randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) swap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash, expireOffset, timestamp, kavaAddrs[0], - kavaAddrs[1], binanceAddrs[0].String(), binanceAddrs[1].String(), 0, types.Open, true, types.Incoming) + kavaAddrs[1], binanceAddrs[0].String(), binanceAddrs[1].String(), 0, types.Open, true, types.Incoming) return swap } diff --git a/x/bep3/types/events.go b/x/bep3/types/events.go index cb320386..298f3be6 100644 --- a/x/bep3/types/events.go +++ b/x/bep3/types/events.go @@ -1,13 +1,13 @@ package types -// bep3 module event types +// Events for bep3 module const ( - EventTypeCreateAtomicSwap = "createAtomicSwap" - EventTypeDepositAtomicSwap = "depositAtomicSwap" - EventTypeClaimAtomicSwap = "claimAtomicSwap" - EventTypeRefundAtomicSwap = "refundAtomicSwap" + EventTypeCreateAtomicSwap = "create_atomic_swap" + EventTypeClaimAtomicSwap = "claim_atomic_swap" + EventTypeRefundAtomicSwap = "refund_atomic_swap" + EventTypeSwapsExpired = "swaps_expired" - // Common + AttributeValueCategory = ModuleName AttributeKeySender = "sender" AttributeKeyRecipient = "recipient" AttributeKeyAtomicSwapID = "atomic_swap_id" @@ -18,13 +18,9 @@ const ( AttributeKeyAmount = "amount" AttributeKeyExpectedIncome = "expected_income" AttributeKeyDirection = "direction" - - // Claim - AttributeKeyClaimSender = "claim_sender" - AttributeKeyRandomNumber = "random_number" - - // Refund - AttributeKeyRefundSender = "refund_sender" - - AttributeValueCategory = ModuleName + AttributeKeyClaimSender = "claim_sender" + AttributeKeyRandomNumber = "random_number" + AttributeKeyRefundSender = "refund_sender" + AttributeKeyAtomicSwapIDs = "atomic_swap_ids" + AttributeExpirationBlock = "expiration_block" ) diff --git a/x/bep3/types/swap.go b/x/bep3/types/swap.go index 6fee11dc..52a75f6c 100644 --- a/x/bep3/types/swap.go +++ b/x/bep3/types/swap.go @@ -10,16 +10,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// Swap is an interface for handling common actions -type Swap interface { - GetSwapID() tmbytes.HexBytes - GetModuleAccountCoins() sdk.Coins - Validate() error -} - // AtomicSwap contains the information for an atomic swap type AtomicSwap struct { - Swap `json:"swap" yaml:"swap"` Amount sdk.Coins `json:"amount" yaml:"amount"` RandomNumberHash tmbytes.HexBytes `json:"random_number_hash" yaml:"random_number_hash"` ExpireHeight int64 `json:"expire_height" yaml:"expire_height"` @@ -59,8 +51,8 @@ func (a AtomicSwap) GetSwapID() tmbytes.HexBytes { return CalculateSwapID(a.RandomNumberHash, a.Sender, a.SenderOtherChain) } -// GetModuleAccountCoins returns the swap's amount as sdk.Coins -func (a AtomicSwap) GetModuleAccountCoins() sdk.Coins { +// GetCoins returns the swap's amount as sdk.Coins +func (a AtomicSwap) GetCoins() sdk.Coins { return sdk.NewCoins(a.Amount...) } diff --git a/x/bep3/types/swap_test.go b/x/bep3/types/swap_test.go index fb7fcf20..c9eae7f5 100644 --- a/x/bep3/types/swap_test.go +++ b/x/bep3/types/swap_test.go @@ -128,7 +128,7 @@ func (suite *AtomicSwapTestSuite) TestNewAtomicSwap() { if tc.expectPass { suite.Nil(swap.Validate()) - suite.Equal(tc.args.amount, swap.GetModuleAccountCoins()) + suite.Equal(tc.args.amount, swap.GetCoins()) expectedSwapID := types.CalculateSwapID(tc.args.randomNumberHash, tc.args.sender, tc.args.senderOtherChain) suite.Equal(tmbytes.HexBytes(expectedSwapID), swap.GetSwapID()) } else {