Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

## Unreleased
sei-chain
* [#4549](https://github.com/sei-protocol/sei-chain/pull/4549) Keep the tokenfactory `burn_tokens` Wasm message working after v6.8: a contract can still burn a `factory/` denom it created from its own balance, so the Silo and Eris liquid staking hubs and the Wormhole token translator keep their redemption paths. Minting and every other tokenfactory message stay removed.
* [#4536](https://github.com/sei-protocol/sei-chain/pull/4536) Remove the tokenfactory module behind the v6.8 upgrade: module, store, params, protobuf schema, CLI, wasm bindings, the bank `factory/` denom transfer allowlist and tooling are gone; only the tokenfactory Msg types remain decodable so historical blocks still trace. Existing `factory/` balances stay in bank but can no longer be minted or burned, and denoms that had a transfer allowlist become freely transferable. The upgrade deletes at most 1,000 allowlists from the bank store and leaves any beyond that in place, unenforced. App-hash breaking at the upgrade height.
* [#4319](https://github.com/sei-protocol/sei-chain/pull/4319) Remove the oracle module behind the v6.8 upgrade: module, store, protobuf schema, wasm query route and tooling are gone; only the oracle Msg types remain decodable so historical blocks still trace. App-hash breaking at the upgrade height.
* [#4332](https://github.com/sei-protocol/sei-chain/pull/4332) Bump sei-protocol/go-ethereum to v1.15.7-sei-21
Expand Down
48 changes: 48 additions & 0 deletions wasmbinding/burn_tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package wasmbinding

import (
"encoding/json"
"strings"

sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types"
sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors"
wasmkeeper "github.com/sei-protocol/sei-chain/sei-wasmd/x/wasm/keeper"
wasmtypes "github.com/sei-protocol/sei-chain/sei-wasmd/x/wasm/types"
wasmvmtypes "github.com/sei-protocol/sei-chain/sei-wasmvm/types"
)

// legacyBurnTokensMsg is the burn_tokens custom message of the retired tokenfactory module.
type legacyBurnTokensMsg struct {
BurnTokens *struct {
Amount sdk.Coin `json:"amount"`
} `json:"burn_tokens"`
}

// LegacyBurnTokensHandler burns coins from the calling contract's own balance for the
// retired tokenfactory burn_tokens custom message. It accepts only denoms the contract
// created, of the form factory/<contract>/<subdenom>, and leaves every other message to
// the next handler.
func LegacyBurnTokensHandler(burner wasmtypes.Burner) wasmkeeper.MessageHandlerFunc {
return func(ctx sdk.Context, contractAddr sdk.AccAddress, _ string, msg wasmvmtypes.CosmosMsg, _ wasmvmtypes.MessageInfo, _ wasmtypes.CodeInfo) ([]sdk.Event, [][]byte, error) {
var parsed legacyBurnTokensMsg
if msg.Custom == nil || json.Unmarshal(msg.Custom, &parsed) != nil || parsed.BurnTokens == nil {
return nil, nil, wasmtypes.ErrUnknownMsg
}
amount := parsed.BurnTokens.Amount
// Validate dereferences the amount, which is nil when the message leaves it out.
if amount.Amount.IsNil() || amount.Validate() != nil || !amount.IsPositive() {
return nil, nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "burn_tokens amount %s", amount)
}
if !strings.HasPrefix(amount.Denom, "factory/"+contractAddr.String()+"/") {
return nil, nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s was not created by %s", amount.Denom, contractAddr)
}
coins := sdk.NewCoins(amount)
Comment thread
alexander-sei marked this conversation as resolved.
if err := burner.SendCoinsFromAccountToModule(ctx, contractAddr, wasmtypes.ModuleName, coins); err != nil {
return nil, nil, sdkerrors.Wrap(err, "transfer to module")
}
if err := burner.BurnCoins(ctx, wasmtypes.ModuleName, coins); err != nil {
return nil, nil, sdkerrors.Wrap(err, "burn coins")
}
return nil, nil, nil
}
}
3 changes: 3 additions & 0 deletions wasmbinding/message_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ func CustomMessageHandler(
Custom: CustomEncoder,
})
return wasmkeeper.NewMessageHandlerChain(
// CustomEncoder refuses burn_tokens with an error that ends the chain, so the
// legacy burn must run before the SDK message handler.
LegacyBurnTokensHandler(bankKeeper),
wasmkeeper.NewSDKMessageHandler(&CustomRouter{MessageRouter: router, evmKeeper: evmKeeper}, encoders),
wasmkeeper.NewBurnCoinMessageHandler(bankKeeper),
)
Expand Down
112 changes: 112 additions & 0 deletions wasmbinding/test/burn_tokens_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package wasmbinding

import (
"fmt"
"testing"

sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types"
sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors"
wasmtypes "github.com/sei-protocol/sei-chain/sei-wasmd/x/wasm/types"
wasmvmtypes "github.com/sei-protocol/sei-chain/sei-wasmvm/types"
"github.com/sei-protocol/sei-chain/wasmbinding"
minttypes "github.com/sei-protocol/sei-chain/x/mint/types"
"github.com/stretchr/testify/require"
)

var legacyBurnContract = sdk.AccAddress("legacy-burn-contract")

// legacyBurnTokens returns the burn_tokens custom message a contract built on
// sei-cosmwasm sends for amount of denom.
func legacyBurnTokens(denom, amount string) wasmvmtypes.CosmosMsg {
return wasmvmtypes.CosmosMsg{Custom: []byte(fmt.Sprintf(`{"burn_tokens":{"amount":{"denom":%q,"amount":%q}}}`, denom, amount))}
}

func TestLegacyBurnTokensBurnsTheContractsOwnBalance(t *testing.T) {
testWrapper, _ := SetupWasmbindingTest(t)
ctx, bank := testWrapper.Ctx, testWrapper.App.BankKeeper
denom := "factory/" + legacyBurnContract.String() + "/stake"
mintLegacyBurnBalance(t, bank, ctx, sdk.NewInt64Coin(denom, 100))

_, _, err := wasmbinding.LegacyBurnTokensHandler(bank).
DispatchMsg(ctx, legacyBurnContract, "", legacyBurnTokens(denom, "40"), wasmvmtypes.MessageInfo{}, wasmtypes.CodeInfo{})
require.NoError(t, err)
require.Equal(t, sdk.NewInt64Coin(denom, 60), bank.GetBalance(ctx, legacyBurnContract, denom))
require.Equal(t, sdk.NewInt64Coin(denom, 60), bank.GetSupply(ctx, denom))
}

func TestLegacyBurnTokensRefusesWhatItCannotBurn(t *testing.T) {
testWrapper, _ := SetupWasmbindingTest(t)
ctx, bank := testWrapper.Ctx, testWrapper.App.BankKeeper
own := "factory/" + legacyBurnContract.String() + "/stake"
foreign := "factory/" + sdk.AccAddress("another-denom-creatr").String() + "/stake"
mintLegacyBurnBalance(t, bank, ctx, sdk.NewInt64Coin(own, 100))
mintLegacyBurnBalance(t, bank, ctx, sdk.NewInt64Coin(foreign, 100))
mintLegacyBurnBalance(t, bank, ctx, sdk.NewInt64Coin("usei", 100))

for _, tc := range []struct {
name string
msg wasmvmtypes.CosmosMsg
error *sdkerrors.Error
}{
{"another creator's denom", legacyBurnTokens(foreign, "1"), sdkerrors.ErrUnauthorized},
{"a native denom", legacyBurnTokens("usei", "1"), sdkerrors.ErrUnauthorized},
{"a zero amount", legacyBurnTokens(own, "0"), sdkerrors.ErrInvalidCoins},
{"a missing amount", wasmvmtypes.CosmosMsg{Custom: []byte(fmt.Sprintf(`{"burn_tokens":{"amount":{"denom":%q}}}`, own))}, sdkerrors.ErrInvalidCoins},
{"more than the balance", legacyBurnTokens(own, "101"), sdkerrors.ErrInsufficientFunds},
} {
t.Run(tc.name, func(t *testing.T) {
_, _, err := wasmbinding.LegacyBurnTokensHandler(bank).
DispatchMsg(ctx, legacyBurnContract, "", tc.msg, wasmvmtypes.MessageInfo{}, wasmtypes.CodeInfo{})
require.ErrorIs(t, err, tc.error)
})
}
for _, denom := range []string{own, foreign, "usei"} {
require.Equal(t, sdk.NewInt64Coin(denom, 100), bank.GetBalance(ctx, legacyBurnContract, denom), denom)
}
}

func TestLegacyBurnTokensLeavesOtherMessagesToTheNextHandler(t *testing.T) {
testWrapper, _ := SetupWasmbindingTest(t)
handler := wasmbinding.LegacyBurnTokensHandler(testWrapper.App.BankKeeper)
for _, msg := range []wasmvmtypes.CosmosMsg{
{},
{Bank: &wasmvmtypes.BankMsg{Burn: &wasmvmtypes.BurnMsg{Amount: wasmvmtypes.Coins{{Denom: "usei", Amount: "1"}}}}},
{Custom: []byte(`{"mint_tokens":{"amount":{"denom":"factory/creator/subdenom","amount":"1"}}}`)},
{Custom: []byte(`{"call_evm":{"to":"0x0000000000000000000000000000000000000001","data":""}}`)},
{Custom: []byte(`not json`)},
} {
_, _, err := handler.DispatchMsg(testWrapper.Ctx, legacyBurnContract, "", msg, wasmvmtypes.MessageInfo{}, wasmtypes.CodeInfo{})
require.ErrorIs(t, err, wasmtypes.ErrUnknownMsg, string(msg.Custom))
}
}

// TestCustomMessageHandlerServesLegacyBurnTokens requires the Sei Wasm message
// handler to burn for burn_tokens while still refusing the other retired
// tokenfactory messages.
func TestCustomMessageHandlerServesLegacyBurnTokens(t *testing.T) {
testWrapper, _ := SetupWasmbindingTest(t)
ctx, bank := testWrapper.Ctx, testWrapper.App.BankKeeper
denom := "factory/" + legacyBurnContract.String() + "/stake"
mintLegacyBurnBalance(t, bank, ctx, sdk.NewInt64Coin(denom, 100))
messenger := wasmbinding.CustomMessageHandler(
testWrapper.App.MsgServiceRouter(), bank, &testWrapper.App.EvmKeeper, testWrapper.App.AppCodec())

_, _, err := messenger.DispatchMsg(ctx, legacyBurnContract, "", legacyBurnTokens(denom, "25"), wasmvmtypes.MessageInfo{}, wasmtypes.CodeInfo{})
require.NoError(t, err)
require.Equal(t, sdk.NewInt64Coin(denom, 75), bank.GetSupply(ctx, denom))

mint := wasmvmtypes.CosmosMsg{Custom: []byte(fmt.Sprintf(`{"mint_tokens":{"amount":{"denom":%q,"amount":"1"}}}`, denom))}
_, _, err = messenger.DispatchMsg(ctx, legacyBurnContract, "", mint, wasmvmtypes.MessageInfo{}, wasmtypes.CodeInfo{})
require.Equal(t, wasmvmtypes.UnsupportedRequest{Kind: "Unknown Sei Wasm Message"}, err)
require.Equal(t, sdk.NewInt64Coin(denom, 75), bank.GetSupply(ctx, denom))
}

func mintLegacyBurnBalance(t *testing.T, bank interface {
MintCoins(sdk.Context, string, sdk.Coins) error
SendCoinsFromModuleToAccount(sdk.Context, string, sdk.AccAddress, sdk.Coins) error
}, ctx sdk.Context, coin sdk.Coin) {
t.Helper()
coins := sdk.NewCoins(coin)
require.NoError(t, bank.MintCoins(ctx, minttypes.ModuleName, coins))
require.NoError(t, bank.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, legacyBurnContract, coins))
}
Loading