diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 12a88490515..34b438acfaa 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -122,6 +122,8 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { initialState: cfg.InitialTxs, currentStateIndex: len(cfg.InitialTxs), } + + // generate genesis state genesis := gnoland.DefaultGenState() genesis.Balances = cfg.BalancesList genesis.Txs = append(pkgsTxs, cfg.InitialTxs...) diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 3f996bc7716..2ac3044d026 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -131,7 +131,6 @@ func (n *Node) ExportStateAsGenesis(ctx context.Context) (*bft.GenesisDoc, error // Get current blockstore state doc := *n.Node.GenesisDoc() // copy doc - genState := doc.AppState.(gnoland.GnoGenesisState) genState.Balances = n.config.BalancesList genState.Txs = state diff --git a/examples/gno.land/r/sys/params/params.gno b/examples/gno.land/r/sys/params/params.gno index fa04c90de3f..d8b73b4a883 100644 --- a/examples/gno.land/r/sys/params/params.gno +++ b/examples/gno.land/r/sys/params/params.gno @@ -52,3 +52,13 @@ func newPropExecutor(key string, fn func()) dao.Executor { } return bridge.GovDAO().NewGovDAOExecutor(callback) } +func propose(exec dao.Executor, title, desc string) uint64 { + // The executor's callback function is executed only after the proposal has been voted on + // and approved by the GovDAO. + prop := dao.ProposalRequest{ + Title: title, + Description: desc, + Executor: exec, + } + return bridge.GovDAO().Propose(prop) +} diff --git a/examples/gno.land/r/sys/params/unlock.gno b/examples/gno.land/r/sys/params/unlock.gno new file mode 100644 index 00000000000..bd6813c7d0b --- /dev/null +++ b/examples/gno.land/r/sys/params/unlock.gno @@ -0,0 +1,17 @@ +package params + +const ( + lockSendKey = "bank_lockSend.string" + UnlockSendTitle = "Proposal to unlock the sending functionality for ugnot." + LockSendTitle = "Proposal to lock the sending functionality for ugnot." +) + +func ProposeUnlockSend() uint64 { + exe := NewStringPropExecutor(lockSendKey, "") + return propose(exe, UnlockSendTitle, "") +} + +func ProposeLockSend() uint64 { + exe := NewStringPropExecutor(lockSendKey, "ugnot") + return propose(exe, LockSendTitle, "") +} diff --git a/examples/gno.land/r/sys/params/unlock_test.gno b/examples/gno.land/r/sys/params/unlock_test.gno new file mode 100644 index 00000000000..8936eb8e7f8 --- /dev/null +++ b/examples/gno.land/r/sys/params/unlock_test.gno @@ -0,0 +1,52 @@ +package params + +import ( + "testing" + + "gno.land/p/demo/dao" + "gno.land/p/demo/simpledao" + "gno.land/p/demo/urequire" + "gno.land/r/gov/dao/bridge" +) + +func TestProUnlockSend(t *testing.T) { + govdao := bridge.GovDAO() + id := ProposeUnlockSend() + p, err := govdao.GetPropStore().ProposalByID(id) + urequire.NoError(t, err) + urequire.Equal(t, UnlockSendTitle, p.Title()) +} + +func TestFailUnlockSend(t *testing.T) { + govdao := bridge.GovDAO() + id := ProposeUnlockSend() + urequire.PanicsWithMessage( + t, + simpledao.ErrProposalNotAccepted.Error(), + func() { + govdao.ExecuteProposal(id) + }, + ) +} + +func TestExeUnlockSend(t *testing.T) { + govdao := bridge.GovDAO() + id := ProposeUnlockSend() + p, err := govdao.GetPropStore().ProposalByID(id) + urequire.NoError(t, err) + urequire.True(t, dao.Active == p.Status()) + + govdao.VoteOnProposal(id, dao.YesVote) + + urequire.True(t, dao.Accepted == p.Status()) + + urequire.NotPanics( + t, + func() { + govdao.ExecuteProposal(id) + }, + ) + + urequire.True(t, dao.ExecutionSuccessful == p.Status()) + +} diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 0826071b9f5..629eb10b389 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -96,14 +96,18 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) // Construct keepers. - paramsKpr := params.NewParamsKeeper(mainKey, "vm") + + paramsKpr := params.NewParamsKeeper(mainKey) acctKpr := auth.NewAccountKeeper(mainKey, paramsKpr, ProtoGnoAccount) + bankKpr := bank.NewBankKeeper(acctKpr, paramsKpr) gpKpr := auth.NewGasPriceKeeper(mainKey) - bankKpr := bank.NewBankKeeper(acctKpr) - vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) vmk.Output = cfg.VMOutput + paramsKpr.Register(acctKpr.GetParamfulKey(), acctKpr) + paramsKpr.Register(bankKpr.GetParamfulKey(), bankKpr) + paramsKpr.Register(vmk.GetParamfulKey(), vmk) + // Set InitChainer icc := cfg.InitChainerConfig icc.baseApp = baseApp @@ -123,7 +127,6 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { ) { // Add last gas price in the context ctx = ctx.WithValue(auth.GasPriceContextKey{}, gpKpr.LastGasPrice(ctx)) - // Override auth params. ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx)) // Continue on with default auth ante handler. @@ -325,11 +328,8 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci if !ok { return nil, fmt.Errorf("invalid AppState of type %T", appState) } - cfg.acctKpr.InitGenesis(ctx, state.Auth) - params := cfg.acctKpr.GetParams(ctx) - ctx = ctx.WithValue(auth.AuthParamsContextKey{}, params) - auth.InitChainer(ctx, cfg.gpKpr.(auth.GasPriceKeeper), params.InitialGasPrice) + cfg.bankKpr.InitGenesis(ctx, state.Bank) // Apply genesis balances. for _, bal := range state.Balances { acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) @@ -344,6 +344,25 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci for _, param := range state.Params { param.register(ctx, cfg.paramsKpr) } + // The account keeper's initial genesis state must be set after genesis + // accounts are created in account keeeper with genesis balances + cfg.acctKpr.InitGenesis(ctx, state.Auth) + + // The unrestricted address must have been created as one of the genesis accounts. + // Otherwise, we cannot verify the unrestricted address in the genesis state. + + for _, addr := range state.Auth.Params.UnrestrictedAddrs { + acc := cfg.acctKpr.GetAccount(ctx, addr) + accr := acc.(AccountRestricter) + accr.SetUnrestricted() + cfg.acctKpr.SetAccount(ctx, acc) + } + + cfg.vmKpr.InitGenesis(ctx, state.VM) + + params := cfg.acctKpr.GetParams(ctx) + ctx = ctx.WithValue(auth.AuthParamsContextKey{}, params) + auth.InitChainer(ctx, cfg.gpKpr, params.InitialGasPrice) // Replay genesis txs. txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs)) diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 361d7505157..0df51f2bed1 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -114,6 +114,7 @@ func TestNewAppWithOptions(t *testing.T) { {"params/vm/foo.bool", `true`}, {"params/vm/foo.bytes", `"SGkh"`}, // XXX: make this test more readable } + for _, tc := range tcs { qres := bapp.Query(abci.RequestQuery{ Path: tc.path, @@ -216,12 +217,17 @@ func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper cfg := InitChainerConfig{ StdlibDir: stdlibDir, vmKpr: mock, + acctKpr: &mockAuthKeeper{}, + bankKpr: &mockBankKeeper{}, + paramsKpr: &mockParamsKeeper{}, + gpKpr: &mockGasPriceKeeper{}, CacheStdlibLoad: cached, } // Construct keepers. - paramsKpr := params.NewParamsKeeper(iavlCapKey, "") - cfg.acctKpr = auth.NewAccountKeeper(iavlCapKey, paramsKpr, ProtoGnoAccount) - cfg.gpKpr = auth.NewGasPriceKeeper(iavlCapKey) + // paramsKpr := params.NewParamsKeeper(iavlCapKey, params.PrefixKeyMapper{}) + // cfg.paramsKpr = &mockParamsKeeper{} + // cfg.acctKpr = auth.NewAccountKeeper(iavlCapKey, paramsKpr, ProtoGnoAccount) + // cfg.gpKpr = auth.NewGasPriceKeeper(iavlCapKey) cfg.InitChainer(testCtx, abci.RequestInitChain{ AppState: DefaultGenState(), }) @@ -822,12 +828,14 @@ func newGasPriceTestApp(t *testing.T) abci.Application { baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) // Construct keepers. - paramsKpr := params.NewParamsKeeper(mainKey, "") + paramsKpr := params.NewParamsKeeper(mainKey) acctKpr := auth.NewAccountKeeper(mainKey, paramsKpr, ProtoGnoAccount) gpKpr := auth.NewGasPriceKeeper(mainKey) - bankKpr := bank.NewBankKeeper(acctKpr) + bankKpr := bank.NewBankKeeper(acctKpr, paramsKpr) vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) - + paramsKpr.Register(acctKpr.GetParamfulKey(), acctKpr) + paramsKpr.Register(bankKpr.GetParamfulKey(), bankKpr) + paramsKpr.Register(vmk.GetParamfulKey(), vmk) // Set InitChainer icc := cfg.InitChainerConfig icc.baseApp = baseApp diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index a754e7a4644..69718266811 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -13,6 +13,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" "github.com/pelletier/go-toml" ) @@ -200,7 +201,8 @@ func DefaultGenState() GnoGenesisState { Balances: []Balance{}, Txs: []TxWithMetadata{}, Auth: authGen, + Bank: bank.DefaultGenesisState(), + VM: vmm.DefaultGenesisState(), } - return gs } diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go index 62aecaf5278..a7f09bbe47d 100644 --- a/gno.land/pkg/gnoland/mock_test.go +++ b/gno.land/pkg/gnoland/mock_test.go @@ -4,10 +4,15 @@ import ( "log/slog" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/service" + "github.com/gnolang/gno/tm2/pkg/std" ) type ( @@ -113,6 +118,86 @@ func (m *mockVMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { } } +func (m *mockVMKeeper) InitGenesis(ctx sdk.Context, gs vm.GenesisState) { + // TODO: +} + +type mockBankKeeper struct{} + +func (m *mockBankKeeper) InputOutputCoins(ctx sdk.Context, inputs []bank.Input, outputs []bank.Output) error { + return nil +} + +func (m *mockBankKeeper) SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + return nil +} + +func (m *mockBankKeeper) SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + return nil +} + +func (m *mockBankKeeper) SubtractCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) { + return nil, nil +} + +func (m *mockBankKeeper) AddCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) { + return nil, nil +} + +func (m *mockBankKeeper) InitGenesis(ctx sdk.Context, data bank.GenesisState) {} +func (m *mockBankKeeper) GetParams(ctx sdk.Context) bank.Params { return bank.Params{} } +func (m *mockBankKeeper) GetCoins(ctx sdk.Context, addr crypto.Address) std.Coins { return nil } +func (m *mockBankKeeper) SetCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) error { + return nil +} + +func (m *mockBankKeeper) HasCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) bool { + return true +} + +type mockAuthKeeper struct{} + +func (m *mockAuthKeeper) NewAccountWithAddress(ctx sdk.Context, addr crypto.Address) std.Account { + return nil +} +func (m *mockAuthKeeper) GetAccount(ctx sdk.Context, addr crypto.Address) std.Account { return nil } +func (m *mockAuthKeeper) GetAllAccounts(ctx sdk.Context) []std.Account { return nil } +func (m *mockAuthKeeper) SetAccount(ctx sdk.Context, acc std.Account) {} +func (m *mockAuthKeeper) IterateAccounts(ctx sdk.Context, process func(std.Account) bool) {} +func (m *mockAuthKeeper) InitGenesis(ctx sdk.Context, data auth.GenesisState) {} +func (m *mockAuthKeeper) GetParams(ctx sdk.Context) auth.Params { return auth.Params{} } + +type mockParamsKeeper struct{} + +func (m *mockParamsKeeper) GetString(ctx sdk.Context, key string, ptr *string) {} +func (m *mockParamsKeeper) GetInt64(ctx sdk.Context, key string, ptr *int64) {} +func (m *mockParamsKeeper) GetUint64(ctx sdk.Context, key string, ptr *uint64) {} +func (m *mockParamsKeeper) GetBool(ctx sdk.Context, key string, ptr *bool) {} +func (m *mockParamsKeeper) GetBytes(ctx sdk.Context, key string, ptr *[]byte) {} + +func (m *mockParamsKeeper) SetString(ctx sdk.Context, key string, value string) {} +func (m *mockParamsKeeper) SetInt64(ctx sdk.Context, key string, value int64) {} +func (m *mockParamsKeeper) SetUint64(ctx sdk.Context, key string, value uint64) {} +func (m *mockParamsKeeper) SetBool(ctx sdk.Context, key string, value bool) {} +func (m *mockParamsKeeper) SetBytes(ctx sdk.Context, key string, value []byte) {} + +func (m *mockParamsKeeper) Has(ctx sdk.Context, key string) bool { return false } +func (m *mockParamsKeeper) GetRaw(ctx sdk.Context, key string) []byte { return nil } + +func (m *mockParamsKeeper) GetParams(ctx sdk.Context, prefixKey string, key string, target interface{}) (bool, error) { + return true, nil +} + +func (m *mockParamsKeeper) SetParams(ctx sdk.Context, prefixKey string, key string, params interface{}) error { + return nil +} + +type mockGasPriceKeeper struct{} + +func (m *mockGasPriceKeeper) LastGasPrice(ctx sdk.Context) std.GasPrice { return std.GasPrice{} } +func (m *mockGasPriceKeeper) SetGasPrice(ctx sdk.Context, gp std.GasPrice) {} +func (m *mockGasPriceKeeper) UpdateGasPrice(ctx sdk.Context) {} + type ( lastBlockHeightDelegate func() int64 loggerDelegate func() *slog.Logger diff --git a/gno.land/pkg/gnoland/test_common.go b/gno.land/pkg/gnoland/test_common.go new file mode 100644 index 00000000000..b4611706031 --- /dev/null +++ b/gno.land/pkg/gnoland/test_common.go @@ -0,0 +1,53 @@ +package gnoland + +import ( + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/db/memdb" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/sdk/params" + + "github.com/gnolang/gno/tm2/pkg/store" + "github.com/gnolang/gno/tm2/pkg/store/iavl" +) + +type testEnv struct { + ctx sdk.Context + acck auth.AccountKeeper + bank bank.BankKeeper +} + +func setupTestEnv() testEnv { + db := memdb.NewMemDB() + + authCapKey := store.NewStoreKey("authCapKey") + + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(authCapKey, iavl.StoreConstructor, db) + ms.LoadLatestVersion() + paramk := params.NewParamsKeeper(authCapKey) + acck := auth.NewAccountKeeper(authCapKey, paramk, ProtoGnoAccount) + bank := bank.NewBankKeeper(acck, paramk) + paramk.Register(acck.GetParamfulKey(), acck) + paramk.Register(bank.GetParamfulKey(), bank) + + ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{Height: 1, ChainID: "test-chain-id"}, log.NewNoopLogger()) + + ctx = ctx.WithConsensusParams(&abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxTxBytes: 1024, + MaxDataBytes: 1024 * 100, + MaxBlockBytes: 1024 * 100, + MaxGas: 10 * 1000 * 1000, + TimeIotaMS: 10, + }, + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, // XXX + }, + }) + + return testEnv{ctx: ctx, acck: acck, bank: bank} +} diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 66fb2f54e8a..f0d09aaa0ec 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -7,9 +7,11 @@ import ( "fmt" "os" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -18,19 +20,103 @@ var ( ErrBalanceEmptyAmount = errors.New("balance amount is empty") ) +const ( + + // unrestricted allows unrestricted transfers. + unrestricted BitSet = 1 << iota + + // TODO: validatorAccount marks an account as validator. + validatorAccount + + // TODO: realmAccount marks an account as realm. + realmAccount +) + +// bitSet represents a set of flags stored in a 16-bit unsigned integer. +// Each bit in the BitSet corresponds to a specific flag. +type BitSet uint16 + +func (bs BitSet) String() string { + return fmt.Sprintf("%016b", bs) // Show all 16 bits +} + type GnoAccount struct { std.BaseAccount + Attributes BitSet `json:"attributes" yaml:"attributes"` +} + +// validFlags defines the set of all valid flags that can be used with BitSet. +var validFlags = unrestricted | validatorAccount | realmAccount + +func (ga *GnoAccount) setFlag(flag BitSet) { + if !isValidFlag(flag) { + panic(fmt.Sprintf("setFlag: invalid flag %d (binary: %b). Valid flags: %b", flag, flag, validFlags)) + } + ga.Attributes |= flag +} + +func (ga *GnoAccount) clearFlag(flag BitSet) { + if !isValidFlag(flag) { + panic(fmt.Sprintf("clearFlag: invalid flag %d (binary: %b). Valid flags: %b", flag, flag, validFlags)) + } + ga.Attributes &= ^flag +} + +func (ga *GnoAccount) hasFlag(flag BitSet) bool { + if !isValidFlag(flag) { + panic(fmt.Sprintf("hasFlag: invalid flag %d (binary: %b). Valid flags: %b", flag, flag, validFlags)) + } + return ga.Attributes&flag != 0 +} + +// isValidFlag ensures that a given BitSet uses only the allowed subset of bits +// as defined in validFlags. This prevents accidentally setting invalid flags, +// especially since BitSet can represent all 16 bits of a uint16. +func isValidFlag(flag BitSet) bool { + return flag&^validFlags == 0 +} + +// SetUnrestricted allows the account to bypass global transfer locking restrictions. +// By default, accounts are restricted when global transfer locking is enabled. +func (ga *GnoAccount) SetUnrestricted() { + ga.setFlag(unrestricted) +} + +// SetRestricted restricts the account when global transfer locking is enabled. +func (ga *GnoAccount) SetRestricted() { + ga.clearFlag(unrestricted) +} + +// IsRestricted checks whether the account is restricted. +func (ga *GnoAccount) IsRestricted() bool { + return !ga.hasFlag(unrestricted) +} + +// String implements fmt.Stringer +func (ga *GnoAccount) String() string { + return fmt.Sprintf("%s\n Attributes: %s", + ga.BaseAccount.String(), + ga.Attributes.String(), + ) } func ProtoGnoAccount() std.Account { return &GnoAccount{} } +type AccountRestricter interface { + IsRestricted() bool + SetUnrestricted() + SetRestricted() +} + type GnoGenesisState struct { Balances []Balance `json:"balances"` Txs []TxWithMetadata `json:"txs"` - Params []Param `json:"params"` Auth auth.GenesisState `json:"auth"` + Bank bank.GenesisState `json:"bank"` + VM vm.GenesisState `json:"vm"` + Params []Param `json:"params"` } type TxWithMetadata struct { diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go index c501325bc3e..5e2174cc5ae 100644 --- a/gno.land/pkg/gnoland/types_test.go +++ b/gno.land/pkg/gnoland/types_test.go @@ -131,6 +131,98 @@ func TestReadGenesisTxs(t *testing.T) { }) } +func TestGnoAccountRestriction(t *testing.T) { + testEnv := setupTestEnv() + ctx, acckpr, bankpr := testEnv.ctx, testEnv.acck, testEnv.bank + + fromAddress := crypto.AddressFromPreimage([]byte("from")) + toAddress := crypto.AddressFromPreimage([]byte("to")) + fromAccount := acckpr.NewAccountWithAddress(ctx, fromAddress) + toAccount := acckpr.NewAccountWithAddress(ctx, toAddress) + + // Unrestrict Account + fromAccount.(AccountRestricter).SetUnrestricted() + assert.False(t, fromAccount.(AccountRestricter).IsRestricted()) + + // Persisted unrestricted state + acckpr.SetAccount(ctx, fromAccount) + fromAccount = acckpr.GetAccount(ctx, fromAddress) + assert.False(t, fromAccount.(AccountRestricter).IsRestricted()) + + // Restrict Account + fromAccount.(AccountRestricter).SetRestricted() + assert.True(t, fromAccount.(AccountRestricter).IsRestricted()) + + // Persisted restricted state + acckpr.SetAccount(ctx, fromAccount) + fromAccount = acckpr.GetAccount(ctx, fromAddress) + assert.True(t, fromAccount.(AccountRestricter).IsRestricted()) + + // Send Unrestricted + fromAccount.SetCoins(std.NewCoins(std.NewCoin("foocoin", 10))) + acckpr.SetAccount(ctx, fromAccount) + acckpr.SetAccount(ctx, toAccount) + + err := bankpr.SendCoins(ctx, fromAddress, toAddress, std.NewCoins(std.NewCoin("foocoin", 3))) + require.NoError(t, err) + balance := acckpr.GetAccount(ctx, toAddress).GetCoins() + assert.Equal(t, balance.String(), "3foocoin") + + // Send Restricted + bankpr.AddRestrictedDenoms(ctx, "foocoin") + err = bankpr.SendCoins(ctx, fromAddress, toAddress, std.NewCoins(std.NewCoin("foocoin", 3))) + require.Error(t, err) + assert.Equal(t, "restricted token transfer error", err.Error()) +} + +func TestGnoAccountSendRestrictions(t *testing.T) { + testEnv := setupTestEnv() + ctx, acckpr, bankpr := testEnv.ctx, testEnv.acck, testEnv.bank + + bankpr.AddRestrictedDenoms(ctx, "foocoin") + addr := crypto.AddressFromPreimage([]byte("addr1")) + addr2 := crypto.AddressFromPreimage([]byte("addr2")) + acc := acckpr.NewAccountWithAddress(ctx, addr) + + // All accounts are restricted by default when the transfer restriction is applied. + + // Test GetCoins/SetCoins + acckpr.SetAccount(ctx, acc) + require.True(t, bankpr.GetCoins(ctx, addr).IsEqual(std.NewCoins())) + + bankpr.SetCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 10))) + require.True(t, bankpr.GetCoins(ctx, addr).IsEqual(std.NewCoins(std.NewCoin("foocoin", 10)))) + + // Test HasCoins + require.True(t, bankpr.HasCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 10)))) + require.True(t, bankpr.HasCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 5)))) + require.False(t, bankpr.HasCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 15)))) + require.False(t, bankpr.HasCoins(ctx, addr, std.NewCoins(std.NewCoin("barcoin", 5)))) + + bankpr.SetCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 15))) + + // Test sending coins restricted to locked accounts. + err := bankpr.SendCoins(ctx, addr, addr2, std.NewCoins(std.NewCoin("foocoin", 5))) + require.ErrorIs(t, err, std.RestrictedTransferError{}, "expected restricted transfer error, got %v", err) + require.True(t, bankpr.GetCoins(ctx, addr).IsEqual(std.NewCoins(std.NewCoin("foocoin", 15)))) + require.True(t, bankpr.GetCoins(ctx, addr2).IsEqual(std.NewCoins(std.NewCoin("foocoin", 0)))) + + // Test sending coins unrestricted to locked accounts. + bankpr.AddCoins(ctx, addr, std.NewCoins(std.NewCoin("barcoin", 30))) + err = bankpr.SendCoins(ctx, addr, addr2, std.NewCoins(std.NewCoin("barcoin", 10))) + require.NoError(t, err) + require.True(t, bankpr.GetCoins(ctx, addr).IsEqual(std.NewCoins(std.NewCoin("barcoin", 20), std.NewCoin("foocoin", 15)))) + require.True(t, bankpr.GetCoins(ctx, addr2).IsEqual(std.NewCoins(std.NewCoin("barcoin", 10)))) + + // Remove the restrictions + bankpr.DelAllRestrictedDenoms(ctx) + // Test sending coins restricted to locked accounts. + err = bankpr.SendCoins(ctx, addr, addr2, std.NewCoins(std.NewCoin("foocoin", 5))) + require.NoError(t, err) + require.True(t, bankpr.GetCoins(ctx, addr).IsEqual(std.NewCoins(std.NewCoin("barcoin", 20), std.NewCoin("foocoin", 10)))) + require.True(t, bankpr.GetCoins(ctx, addr2).IsEqual(std.NewCoins(std.NewCoin("barcoin", 10), std.NewCoin("foocoin", 5)))) +} + func TestSignGenesisTx(t *testing.T) { t.Parallel() @@ -156,3 +248,27 @@ func TestSignGenesisTx(t *testing.T) { assert.True(t, pubKey.VerifyBytes(payload, sigs[0].Signature)) } } + +func TestSetFlag(t *testing.T) { + account := &GnoAccount{} + + // Test setting a valid flag + account.setFlag(unrestricted) + assert.True(t, account.hasFlag(unrestricted), "Expected unrestricted flag to be set") + + // Test setting an invalid flag + assert.Panics(t, func() { + account.setFlag(BitSet(0x1000)) // Invalid flag + }, "Expected panic for invalid flag") +} + +func TestClearFlag(t *testing.T) { + account := &GnoAccount{} + + // Set and then clear the flag + account.setFlag(unrestricted) + assert.True(t, account.hasFlag(unrestricted), "Expected unrestricted flag to be set before clearing") + + account.clearFlag(unrestricted) + assert.False(t, account.hasFlag(unrestricted), "Expected unrestricted flag to be cleared") +} diff --git a/gno.land/pkg/integration/node_testing.go b/gno.land/pkg/integration/node_testing.go index edcf53de5d3..5ef0f3f0365 100644 --- a/gno.land/pkg/integration/node_testing.go +++ b/gno.land/pkg/integration/node_testing.go @@ -14,6 +14,8 @@ import ( bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/db/memdb" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/require" ) @@ -58,19 +60,17 @@ func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...gnoland.TxW cfg := TestingMinimalNodeConfig(gnoroot) cfg.SkipGenesisVerification = true - creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 - params := LoadDefaultGenesisParamFile(t, gnoroot) + creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 balances := LoadDefaultGenesisBalanceFile(t, gnoroot) txs := make([]gnoland.TxWithMetadata, 0) txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...) txs = append(txs, additionalTxs...) - - cfg.Genesis.AppState = gnoland.GnoGenesisState{ - Balances: balances, - Txs: txs, - Params: params, - } + ggs := cfg.Genesis.AppState.(gnoland.GnoGenesisState) + ggs.Balances = balances + ggs.Txs = txs + ggs.Params = params + cfg.Genesis.AppState = ggs return cfg, creator } @@ -98,6 +98,20 @@ func TestingMinimalNodeConfig(gnoroot string) *gnoland.InMemoryNodeConfig { } func DefaultTestingGenesisConfig(gnoroot string, self crypto.PubKey, tmconfig *tmcfg.Config) *bft.GenesisDoc { + authGen := auth.DefaultGenesisState() + authGen.Params.UnrestrictedAddrs = []crypto.Address{crypto.MustAddressFromString(DefaultAccount_Address)} + authGen.Params.InitialGasPrice = std.GasPrice{Gas: 0, Price: std.Coin{Amount: 0, Denom: "ugnot"}} + genState := gnoland.DefaultGenState() + genState.Balances = []gnoland.Balance{ + { + Address: crypto.MustAddressFromString(DefaultAccount_Address), + Amount: std.MustParseCoins(ugnot.ValueString(10000000000000)), + }, + } + genState.Txs = []gnoland.TxWithMetadata{} + genState.Params = []gnoland.Param{} + genState.Auth = authGen + genState.Bank = bank.DefaultGenesisState() return &bft.GenesisDoc{ GenesisTime: time.Now(), ChainID: tmconfig.ChainID(), @@ -117,16 +131,7 @@ func DefaultTestingGenesisConfig(gnoroot string, self crypto.PubKey, tmconfig *t Name: "self", }, }, - AppState: gnoland.GnoGenesisState{ - Balances: []gnoland.Balance{ - { - Address: crypto.MustAddressFromString(DefaultAccount_Address), - Amount: std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)), - }, - }, - Txs: []gnoland.TxWithMetadata{}, - Params: []gnoland.Param{}, - }, + AppState: genState, } } diff --git a/gno.land/pkg/integration/testdata/adduserfrom.txtar b/gno.land/pkg/integration/testdata/adduserfrom.txtar index 8bbfaa738fd..a4b71479cae 100644 --- a/gno.land/pkg/integration/testdata/adduserfrom.txtar +++ b/gno.land/pkg/integration/testdata/adduserfrom.txtar @@ -27,7 +27,7 @@ stdout ' "BaseAccount": {' stdout ' "address": "g1mtmrdmqfu0aryqfl4aw65n35haw2wdjkh5p4cp",' stdout ' "coins": "10000000ugnot",' stdout ' "public_key": null,' -stdout ' "account_number": "59",' +stdout ' "account_number": "60",' stdout ' "sequence": "0"' stdout ' }' stdout '}' diff --git a/gno.land/pkg/integration/testdata/genesis_params.txtar b/gno.land/pkg/integration/testdata/genesis_params.txtar index d09ededf78a..302542cb993 100644 --- a/gno.land/pkg/integration/testdata/genesis_params.txtar +++ b/gno.land/pkg/integration/testdata/genesis_params.txtar @@ -25,4 +25,3 @@ gnokey query params/vm/gno.land/r/sys/params.test.foo.bool stdout 'data: true' # TODO: Consider adding a test case for a byte array parameter - diff --git a/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar b/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar index 838db121442..c4d0e6bcdf8 100644 --- a/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar +++ b/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar @@ -19,7 +19,7 @@ stdout ' "BaseAccount": {' stdout ' "address": "g1meuazsmy8ztaz2xpuyraqq4axy6s00ycl07zva",' stdout ' "coins": "[0-9]*ugnot",' # dynamic stdout ' "public_key": null,' -stdout ' "account_number": "57",' +stdout ' "account_number": "58",' stdout ' "sequence": "0"' stdout ' }' stdout '}' @@ -30,7 +30,7 @@ gnokey maketx call -pkgpath "gno.land/r/demo/echo" -func "Render" -gas-fee 10000 cp stdout call.tx # Sign -gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 57 -account-sequence 0 user1 +gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 58 -account-sequence 0 user1 cmpenv stdout sign.stdout.golden gnokey broadcast $WORK/call.tx diff --git a/gno.land/pkg/integration/testdata/params.txtar b/gno.land/pkg/integration/testdata/params.txtar index 30363aa6369..1bf07d56dc6 100644 --- a/gno.land/pkg/integration/testdata/params.txtar +++ b/gno.land/pkg/integration/testdata/params.txtar @@ -3,57 +3,64 @@ gnoland start # query before adding the package -gnokey query params/vm/gno.land/r/sys/setter.foo.string +gnokey query params/vm/gno.land/r/sys/params.foo.string stdout 'data: $' -gnokey query params/vm/gno.land/r/sys/setter.bar.bool +gnokey query params/vm/gno.land/r/sys/params.bar.bool stdout 'data: $' -gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +gnokey query params/vm/gno.land/r/sys/params.baz.int64 stdout 'data: $' -gnokey maketx addpkg -pkgdir $WORK/setter -pkgpath gno.land/r/sys/setter -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/params -pkgpath gno.land/r/sys/params -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 # query after adding the package, but before setting values -gnokey query params/vm/gno.land/r/sys/setter.foo.string +gnokey query params/vm/gno.land/r/sys/params.foo.string stdout 'data: $' -gnokey query params/vm/gno.land/r/sys/setter.bar.bool +gnokey query params/vm/gno.land/r/sys/params.bar.bool stdout 'data: $' -gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +gnokey query params/vm/gno.land/r/sys/params.baz.int64 stdout 'data: $' # set foo (string) -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetFoo -args foo1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.foo.string +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetFoo -args foo1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/params.foo.string stdout 'data: "foo1"' # override foo -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetFoo -args foo2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.foo.string +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetFoo -args foo2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/params.foo.string stdout 'data: "foo2"' # set bar (bool) -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBar -args true -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.bar.bool +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetBar -args true -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/params.bar.bool stdout 'data: true' -# override bar -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBar -args false -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.bar.bool +# override bar +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetBar -args false -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/params.bar.bool stdout 'data: false' # set baz (bool) -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBaz -args 1337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetBaz -args 1337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/params.baz.int64 stdout 'data: "1337"' # override baz -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBaz -args 31337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetBaz -args 31337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/params.baz.int64 stdout 'data: "31337"' --- setter/setter.gno -- + +# set parameters in module vm gno.land/r/sys/params.vm. + +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetLockSend -args ugnot -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/params.bank_lockSend.string +stdout 'data: "ugnot"' + +-- params/setter.gno -- package setter import ( @@ -63,3 +70,5 @@ import ( func SetFoo(newFoo string) { std.SetParamString("foo.string", newFoo) } func SetBar(newBar bool) { std.SetParamBool("bar.bool", newBar) } func SetBaz(newBaz int64) { std.SetParamInt64("baz.int64", newBaz) } + +func SetLockSend(denom string) { std.SetParamString("bank_lockSend.string", "ugnot") } diff --git a/gno.land/pkg/integration/testdata/restart_missing_type.txtar b/gno.land/pkg/integration/testdata/restart_missing_type.txtar index cc8ed702734..ffb47a4ac2d 100644 --- a/gno.land/pkg/integration/testdata/restart_missing_type.txtar +++ b/gno.land/pkg/integration/testdata/restart_missing_type.txtar @@ -9,15 +9,15 @@ stdout 'g1lmgyf29g6zqgpln5pq05zzt7qkz2wga7xgagv4' loadpkg gno.land/p/demo/avl gnoland start -gnokey sign -tx-path $WORK/tx1.tx -chainid tendermint_test -account-sequence 0 -account-number 57 user1 +gnokey sign -tx-path $WORK/tx1.tx -chainid tendermint_test -account-sequence 0 -account-number 58 user1 ! gnokey broadcast $WORK/tx1.tx stderr 'out of gas' -gnokey sign -tx-path $WORK/tx2.tx -chainid tendermint_test -account-sequence 1 -account-number 57 user1 +gnokey sign -tx-path $WORK/tx2.tx -chainid tendermint_test -account-sequence 1 -account-number 58 user1 gnokey broadcast $WORK/tx2.tx stdout 'OK!' -gnokey sign -tx-path $WORK/tx3.tx -chainid tendermint_test -account-sequence 2 -account-number 57 user1 +gnokey sign -tx-path $WORK/tx3.tx -chainid tendermint_test -account-sequence 2 -account-number 58 user1 gnokey broadcast $WORK/tx3.tx stdout 'OK!' diff --git a/gno.land/pkg/integration/testdata/simulate_gas.txtar b/gno.land/pkg/integration/testdata/simulate_gas.txtar index 0dcb9ba424b..4fe8a690adf 100644 --- a/gno.land/pkg/integration/testdata/simulate_gas.txtar +++ b/gno.land/pkg/integration/testdata/simulate_gas.txtar @@ -6,11 +6,11 @@ gnoland start # simulate only gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1 -stdout 'GAS USED: 99371' +stdout 'GAS USED: 99587' # simulate skip gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1 -stdout 'GAS USED: 99371' # same as simulate only +stdout 'GAS USED: 99587' # same as simulate only -- package/package.gno -- diff --git a/gno.land/pkg/integration/testdata/transfer_lock.txtar b/gno.land/pkg/integration/testdata/transfer_lock.txtar new file mode 100644 index 00000000000..68defdb53db --- /dev/null +++ b/gno.land/pkg/integration/testdata/transfer_lock.txtar @@ -0,0 +1,54 @@ +## It tests locking token transfers while allowing the payment of gas fees. + +## locking transfer applies to regular accounts +adduser regular1 + + +loadpkg gno.land/r/demo/wugnot +loadpkg gno.land/r/demo/echo + +## start a new node. +## The -lock-transfer flag is intended for integration testing purposes +## and is not a valid application flag for gnoland. + +gnoland start -lock-transfer + +## User test1 is an unrestricted account specified in the genesis state +gnokey maketx send -send "9999999ugnot" -to $regular1_user_addr -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 + +stdout 'OK!' + +## Restricted simple token transfer +! gnokey maketx send -send "9999999ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test regular1 +stderr 'restricted token transfer error' + +## Restricted token transfer by calling a realm deposit function. +! gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -gas-fee 1000000ugnot -send "10000ugnot" -gas-wanted 2000000 -broadcast -chainid=tendermint_test regular1 +stderr 'restricted token transfer error' + + +## Restricted token transfer with depositing to a realm package while adding a package. +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/bank -deposit "1000ugnot" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test regular1 +stderr 'restricted token transfer error' + +## paying gas fees to add a package is acceptable. +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/bank -gas-fee 1000000ugnot -gas-wanted 12500000 -broadcast -chainid=tendermint_test regular1 +stdout 'OK!' + +## paying gas fees to call a realm function is acceptable. +gnokey maketx call -pkgpath gno.land/r/demo/echo -func Render -args "Hello!" -gas-fee 1000000ugnot -gas-wanted 12500000 -broadcast -chainid=tendermint_test regular1 +stdout 'Hello!' + +-- bank.gno -- +package bank +import ( +"std" +) +func Withdraw(denom string, amt int64) string{ + caller := std.GetOrigCaller() + coin := std.Coins{{denom, amt}} + banker := std.GetBanker(std.BankerTypeOrigSend) + pkgaddr := std.GetOrigPkgAddr() + banker.SendCoins(pkgaddr, caller, coin) + return "Withdrawed!" +} diff --git a/gno.land/pkg/integration/testdata/transfer_unlock.txtar b/gno.land/pkg/integration/testdata/transfer_unlock.txtar new file mode 100644 index 00000000000..40accf1f89d --- /dev/null +++ b/gno.land/pkg/integration/testdata/transfer_unlock.txtar @@ -0,0 +1,42 @@ +## It tests unlocking token transfers through GovDAO voting +loadpkg gno.land/r/sys/params +loadpkg gno.land/r/gov/dao/v2 + +patchpkg "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + +adduser regular1 + +## The -lock-transfer flag is not a Gnoland service flag; it is a flag for the txtar setting. +gnoland start -lock-transfer + +## User test1 is an unrestricted account specified in the genesis state +gnokey maketx send -send "9999999ugnot" -to $regular1_user_addr -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 + +stdout 'OK!' + +## Restricted simple token transfer for a regular account +! gnokey maketx send -send "100ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test regular1 + +stderr 'restricted token transfer error' + +## Submit a proposal to unlock the transfer. When token transfer is locked, only the predefined unrestricted account test1 in the genesis state can +## pay the fee and submit a proposal to unlock the transfer. +gnokey maketx call -pkgpath gno.land/r/sys/params -func ProposeUnlockSend -send 250000000ugnot -gas-fee 1ugnot -gas-wanted 9500000 -broadcast -chainid=tendermint_test test1 + +stdout '(0 uint64)' + + +## Vote unlock proposal with unrestricted account test1 +gnokey maketx call -pkgpath gno.land/r/gov/dao/v2 -func VoteOnProposal -args 0 -args "YES" -send 250000000ugnot -gas-fee 1ugnot -gas-wanted 9500000 -broadcast -chainid=tendermint_test test1 + +stdout 'OK!' + +## Execute unlock proposal with unrestricted account test1 +gnokey maketx call -pkgpath gno.land/r/gov/dao/v2 -func ExecuteProposal -args 0 -send 250000000ugnot -gas-fee 1ugnot -gas-wanted 9500000 -broadcast -chainid=tendermint_test test1 + +stdout 'OK!' + +## Restricted transfer is unlocked, allowing simple token transfers for regular accounts. +gnokey maketx send -send "100ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test regular1 + +stdout 'OK!' diff --git a/gno.land/pkg/integration/testscript_gnoland.go b/gno.land/pkg/integration/testscript_gnoland.go index 1531b83dfef..6c048c60318 100644 --- a/gno.land/pkg/integration/testscript_gnoland.go +++ b/gno.land/pkg/integration/testscript_gnoland.go @@ -183,19 +183,17 @@ func SetupGnolandTestscript(t *testing.T, p *testscript.Params) error { env.Setenv("SID", sid) } - balanceFile := LoadDefaultGenesisBalanceFile(t, gnoRootDir) - genesisParamFile := LoadDefaultGenesisParamFile(t, gnoRootDir) - // Track new user balances added via the `adduser` // command and packages added with the `loadpkg` command. // This genesis will be use when node is started. - genesis := &gnoland.GnoGenesisState{ - Balances: balanceFile, - Params: genesisParamFile, - Txs: []gnoland.TxWithMetadata{}, - } - env.Values[envKeyGenesis] = genesis + genesis := gnoland.DefaultGenState() + genesis.Balances = LoadDefaultGenesisBalanceFile(t, gnoRootDir) + genesis.Params = LoadDefaultGenesisParamFile(t, gnoRootDir) + genesis.Auth.Params.InitialGasPrice = std.GasPrice{Gas: 0, Price: std.Coin{Amount: 0, Denom: "ugnot"}} + genesis.Txs = []gnoland.TxWithMetadata{} + + env.Values[envKeyGenesis] = &genesis env.Values[envKeyPkgsLoader] = NewPkgsLoader() env.Setenv("GNOROOT", gnoRootDir) @@ -273,6 +271,7 @@ func gnolandCmd(t *testing.T, nodesManager *NodesManager, gnoRootDir string) fun // directly or use the config command for this. fs := flag.NewFlagSet("start", flag.ContinueOnError) nonVal := fs.Bool("non-validator", false, "set up node as a non-validator") + lockTransfer := fs.Bool("lock-transfer", false, "lock transfer ugnot") if err := fs.Parse(cmdargs); err != nil { ts.Fatalf("unable to parse `gnoland start` flags: %s", err) } @@ -285,10 +284,16 @@ func gnolandCmd(t *testing.T, nodesManager *NodesManager, gnoRootDir string) fun } cfg := TestingMinimalNodeConfig(gnoRootDir) - genesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState) - genesis.Txs = append(pkgsTxs, genesis.Txs...) + tsGenesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState) + genesis := cfg.Genesis.AppState.(gnoland.GnoGenesisState) + genesis.Txs = append(genesis.Txs, append(pkgsTxs, tsGenesis.Txs...)...) + genesis.Balances = append(genesis.Balances, tsGenesis.Balances...) + genesis.Params = append(genesis.Params, tsGenesis.Params...) + if *lockTransfer { + genesis.Bank.Params.RestrictedDenoms = []string{"ugnot"} + } - cfg.Genesis.AppState = *genesis + cfg.Genesis.AppState = genesis if *nonVal { pv := gnoland.NewMockedPrivValidator() cfg.Genesis.Validators = []bft.GenesisValidator{ diff --git a/gno.land/pkg/sdk/vm/builtins.go b/gno.land/pkg/sdk/vm/builtins.go index 161e459873d..9a0d63d6acf 100644 --- a/gno.land/pkg/sdk/vm/builtins.go +++ b/gno.land/pkg/sdk/vm/builtins.go @@ -1,8 +1,13 @@ package vm import ( + "fmt" + "strings" + + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/params" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -60,21 +65,104 @@ func (bnk *SDKBanker) RemoveCoin(b32addr crypto.Bech32Address, denom string, amo // SDKParams type SDKParams struct { - vmk *VMKeeper + pmk *params.ParamsKeeper ctx sdk.Context } -func NewSDKParams(vmk *VMKeeper, ctx sdk.Context) *SDKParams { +// These are the native function implementations bound to standard libraries in Gno. +// All methods of this struct are not intended to be called from outside vm/stdlibs/std. +// +// The key has the format ..: +// realm: A realm path indicating where Set methods are called from. +// paramname: The parameter key. If it contains a prefix that matches the module's paramkey +// prefix (which by default is the module name), it indicates an attempt to set the module's +// parameters for the chain. Otherwise, it is treated as an arbitrary parameter. +// type: The primitive type of the parameter value. + +func NewSDKParams(pmk *params.ParamsKeeper, ctx sdk.Context) *SDKParams { return &SDKParams{ - vmk: vmk, + pmk: pmk, ctx: ctx, } } -func (prm *SDKParams) SetString(key, value string) { prm.vmk.prmk.SetString(prm.ctx, key, value) } -func (prm *SDKParams) SetBool(key string, value bool) { prm.vmk.prmk.SetBool(prm.ctx, key, value) } -func (prm *SDKParams) SetInt64(key string, value int64) { prm.vmk.prmk.SetInt64(prm.ctx, key, value) } +func (prm *SDKParams) SetString(key, value string) { + prm.assertRealmAccess(key) + prm.willSetKeeperParams(prm.ctx, key, value) + prm.pmk.SetString(prm.ctx, key, value) +} + +// Set a boolean parameter in the format of realmPath.parameter.bool +func (prm *SDKParams) SetBool(key string, value bool) { + prm.assertRealmAccess(key) + prm.willSetKeeperParams(prm.ctx, key, value) + prm.pmk.SetBool(prm.ctx, key, value) +} + +func (prm *SDKParams) SetInt64(key string, value int64) { + prm.assertRealmAccess(key) + prm.willSetKeeperParams(prm.ctx, key, value) + prm.pmk.SetInt64(prm.ctx, key, value) +} + func (prm *SDKParams) SetUint64(key string, value uint64) { - prm.vmk.prmk.SetUint64(prm.ctx, key, value) + prm.assertRealmAccess(key) + prm.willSetKeeperParams(prm.ctx, key, value) + prm.pmk.SetUint64(prm.ctx, key, value) +} + +func (prm *SDKParams) SetBytes(key string, value []byte) { + prm.assertRealmAccess(key) + prm.willSetKeeperParams(prm.ctx, key, value) + prm.pmk.SetBytes(prm.ctx, key, value) +} + +// willSetKeeperParams parses the parameter key and sets the keeper it matches the keeper key +// The format is sysParamsRealm.[keeperKeyPrefix_]keyName.keyType +// Ex. gno.lang/r/sys/params.bank_lockSend.string +// The "keeperKeyPrefix_" is optional. If keeperKeyPrefix is presented in the key, +// it must match the keeper names; otherwise it will panic and revert the transaction. +func (prm *SDKParams) willSetKeeperParams(ctx sdk.Context, key string, value interface{}) { + // key is in the format of realm.keeperkey_keyname.type + realmPrefix := gno.ReRealmPath.FindString(key) + if realmPrefix == "" { + panic(fmt.Sprintf("set parameter %s must be accessed from a realm.", key)) + } + // ParamfulKeeper can be accessed from SysParamsRealmPath only + if realmPrefix != SysParamsRealmPath { + return + } + + k, ok := strings.CutPrefix(key, realmPrefix) + if !ok { + return + } + + parts := strings.SplitN(k, ".", 2) + paramKey := parts[1] + parts = strings.SplitN(paramKey, "_", 2) + keeperKeyPrefix := "" + + if len(parts) == 2 { + keeperKeyPrefix = parts[0] + paramKey = parts[1] + } else { + // no keeperKeyPrefix + return + } + + if !prm.pmk.IsRegistered(keeperKeyPrefix) { + panic(fmt.Sprintf("keeper key <%s> does not exist", keeperKeyPrefix)) + } + kpr := prm.pmk.GetRegisteredKeeper(keeperKeyPrefix) + if kpr != nil { + kpr.WillSetParam(prm.ctx, paramKey, value) + } +} + +func (prm *SDKParams) assertRealmAccess(key string) { + realm := gno.ReRealmPath.FindString(key) + if realm == "" { + panic(fmt.Sprintf("Set parameters must be accessed from a realm")) + } } -func (prm *SDKParams) SetBytes(key string, value []byte) { prm.vmk.prmk.SetBytes(prm.ctx, key, value) } diff --git a/gno.land/pkg/sdk/vm/builtins_test.go b/gno.land/pkg/sdk/vm/builtins_test.go new file mode 100644 index 00000000000..87f4bc53b95 --- /dev/null +++ b/gno.land/pkg/sdk/vm/builtins_test.go @@ -0,0 +1,113 @@ +package vm + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParamsRestrictedRealm(t *testing.T) { + env := setupTestEnv() + params := NewSDKParams(&env.vmk.prmk, env.ctx) + + testCases := []struct { + name string + setFunc func() + expectedMsg string + }{ + { + name: "SetString should panic", + setFunc: func() { + params.SetString("gno.land/p/foo.bank.name.string", "foo") + }, + expectedMsg: "Set parameters must be accessed from a realm", + }, + { + name: "SetBool should panic", + setFunc: func() { + params.SetBool("gno.land/p/foo.bank.isFoo.bool", true) + }, + expectedMsg: "Set parameters must be accessed from a realm", + }, + { + name: "SetInt64 should panic", + setFunc: func() { + params.SetInt64("gno.land/p/foo.bank.nummber.int64", -100) + }, + expectedMsg: "Set parameters must be accessed from a realm", + }, + { + name: "SetUint64 should panic", + setFunc: func() { + params.SetUint64("gno.land/p/foo.bank.nummber.uint64", 100) + }, + expectedMsg: "Set parameters must be accessed from a realm", + }, + { + name: "SetBytes should panic", + setFunc: func() { + params.SetBytes("gno.land/p/foo.bank.name.bytes", []byte("foo")) + }, + expectedMsg: "Set parameters must be accessed from a realm", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.PanicsWithValue(t, tc.expectedMsg, tc.setFunc, "The panic message did not match the expected value") + }) + } +} + +func TestParamsKeeper(t *testing.T) { + env := setupTestEnv() + params := NewSDKParams(&env.vmk.prmk, env.ctx) + + testCases := []struct { + name string + setFunc func() + expectedMsg string + }{ + { + name: "SetString should panic", + setFunc: func() { + params.SetString("gno.land/r/sys/params.foo_name.string", "foo") + }, + expectedMsg: `keeper key does not exist`, + }, + { + name: "SetBool should panic", + setFunc: func() { + params.SetBool("gno.land/r/sys/params.foo_isFoo.bool", true) + }, + expectedMsg: `keeper key does not exist`, + }, + { + name: "SetInt64 should panic", + setFunc: func() { + params.SetInt64("gno.land/r/sys/params.foo_nummber.int64", -100) + }, + expectedMsg: `keeper key does not exist`, + }, + { + name: "SetUint64 should panic", + setFunc: func() { + params.SetUint64("gno.land/r/sys/params.foo_nummber.uint64", 100) + }, + expectedMsg: `keeper key does not exist`, + }, + { + name: "SetBytes should panic", + setFunc: func() { + params.SetBytes("gno.land/r/sys/params.foo_name.bytes", []byte("foo")) + }, + expectedMsg: `keeper key does not exist`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.PanicsWithValue(t, tc.expectedMsg, tc.setFunc, "The panic message did not match the expected value") + }) + } +} diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 10402f31f64..ffa53be55e9 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -47,12 +47,16 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { ms.LoadLatestVersion() ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) - prmk := paramsm.NewParamsKeeper(iavlCapKey, "params") - acck := authm.NewAccountKeeper(iavlCapKey, prmk, std.ProtoBaseAccount) - bank := bankm.NewBankKeeper(acck) + prmk := paramsm.NewParamsKeeper(iavlCapKey) + acck := authm.NewAccountKeeper(iavlCapKey, prmk, std.ProtoBaseAccount) + bank := bankm.NewBankKeeper(acck, prmk) vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, prmk) + prmk.Register(acck.GetParamfulKey(), acck) + prmk.Register(bank.GetParamfulKey(), bank) + prmk.Register(vmk.GetParamfulKey(), vmk) + mcw := ms.MultiCacheWrap() vmk.Initialize(log.NewNoopLogger(), mcw) stdlibCtx := vmk.MakeGnoTransactionStore(ctx.WithMultiStore(mcw)) diff --git a/gno.land/pkg/sdk/vm/consts.go b/gno.land/pkg/sdk/vm/consts.go index 292f34a9d20..b3a859e163a 100644 --- a/gno.land/pkg/sdk/vm/consts.go +++ b/gno.land/pkg/sdk/vm/consts.go @@ -1,6 +1,8 @@ package vm const ( - ModuleName = "vm" - RouterKey = ModuleName + ModuleName = "vm" + RouterKey = ModuleName + ParamsKeyPrefixy = ModuleName + SysParamsRealmPath = "gno.land/r/sys/params" ) diff --git a/gno.land/pkg/sdk/vm/genesis.go b/gno.land/pkg/sdk/vm/genesis.go new file mode 100644 index 00000000000..29db3e83cc4 --- /dev/null +++ b/gno.land/pkg/sdk/vm/genesis.go @@ -0,0 +1,48 @@ +package vm + +import ( + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/sdk" +) + +// GenesisState - all state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params" yaml:"params"` +} + +// NewGenesisState - Create a new genesis state +func NewGenesisState(params Params) GenesisState { + return GenesisState{params} +} + +// DefaultGenesisState - Return a default genesis state +func DefaultGenesisState() GenesisState { + return NewGenesisState(DefaultParams()) +} + +// ValidateGenesis performs basic validation of genesis data returning an +// error for any failed validation criteria. +func ValidateGenesis(data GenesisState) error { + return data.Params.Validate() +} + +// InitGenesis - Init store state from genesis data +func (vm *VMKeeper) InitGenesis(ctx sdk.Context, data GenesisState) { + if amino.DeepEqual(data, GenesisState{}) { + return + } + + if err := ValidateGenesis(data); err != nil { + panic(err) + } + if err := vm.SetParams(ctx, data.Params); err != nil { + panic(err) + } +} + +// ExportGenesis returns a GenesisState for a given context and keeper +func (vm *VMKeeper) ExportGenesis(ctx sdk.Context) GenesisState { + params := vm.GetParams(ctx) + + return NewGenesisState(params) +} diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index bf16cd44243..2c9f4709631 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -52,6 +52,7 @@ type VMKeeperI interface { LoadStdlibCached(ctx sdk.Context, stdlibDir string) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context CommitGnoTransactionStore(ctx sdk.Context) + InitGenesis(ctx sdk.Context, data GenesisState) } var _ VMKeeperI = &VMKeeper{} @@ -274,7 +275,7 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add OrigPkgAddr: pkgAddr.Bech32(), // XXX: should we remove the banker ? Banker: NewSDKBanker(vm, ctx), - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } @@ -379,7 +380,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { OrigSendSpent: new(std.Coins), OrigPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } // Parse and run the files, construct *PV. @@ -470,7 +471,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { OrigSendSpent: new(std.Coins), OrigPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } // Construct machine and evaluate. @@ -585,7 +586,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { OrigSendSpent: new(std.Coins), OrigPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } @@ -738,7 +739,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res // OrigSendSpent: nil, OrigPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } m := gno.NewMachineWithOptions( @@ -795,7 +796,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string // OrigSendSpent: nil, OrigPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } m := gno.NewMachineWithOptions( diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index f8144988c44..aa8040c1d8f 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -200,7 +200,6 @@ func GetAdmin() string { res, err := env.vmk.Call(ctx, msg2) assert.Error(t, err) assert.Equal(t, "", res) - fmt.Println(err.Error()) assert.True(t, strings.Contains(err.Error(), "insufficient coins error")) } @@ -352,7 +351,7 @@ func TestVMKeeperParams(t *testing.T) { // Create test package. files := []*gnovm.MemFile{ {Name: "init.gno", Body: ` -package test +package params import "std" @@ -367,7 +366,7 @@ func Do() string { return "XXX" // return std.GetConfig("gno.land/r/test.foo"), if we want to expose std.GetConfig, maybe as a std.TestGetConfig }`}, } - pkgPath := "gno.land/r/test" + pkgPath := SysParamsRealmPath msg1 := NewMsgAddPackage(addr, pkgPath, files) err := env.vmk.AddPackage(ctx, msg1) assert.NoError(t, err) @@ -384,8 +383,8 @@ func Do() string { var foo string var bar int64 - env.vmk.prmk.GetString(ctx, "gno.land/r/test.foo.string", &foo) - env.vmk.prmk.GetInt64(ctx, "gno.land/r/test.bar.int64", &bar) + env.vmk.prmk.GetString(ctx, "gno.land/r/sys/params.foo.string", &foo) + env.vmk.prmk.GetInt64(ctx, "gno.land/r/sys/params.bar.int64", &bar) assert.Equal(t, "foo2", foo) assert.Equal(t, int64(1337), bar) } diff --git a/gno.land/pkg/sdk/vm/params.go b/gno.land/pkg/sdk/vm/params.go index 248fb8a81fb..ddddea6a3a9 100644 --- a/gno.land/pkg/sdk/vm/params.go +++ b/gno.land/pkg/sdk/vm/params.go @@ -1,10 +1,78 @@ package vm -import "github.com/gnolang/gno/tm2/pkg/sdk" +import ( + "fmt" + "strings" + + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/sdk" +) + +const ( + sysUsersPkgDefault = "gno.land/r/sys/users" + paramsKey = "p" +) + +// Params defines the parameters for the bank module. +type Params struct { + SysUsersPkg string `json:"sysusers_pkgpath" yaml:"sysusers_pkgpath"` +} + +// NewParams creates a new Params object +func NewParams(pkgPath string) Params { + return Params{ + SysUsersPkg: pkgPath, + } +} + +// DefaultParams returns a default set of parameters. +func DefaultParams() Params { + return NewParams(sysUsersPkgDefault) +} + +// String implements the stringer interface. +func (p Params) String() string { + var sb strings.Builder + sb.WriteString("Params: \n") + sb.WriteString(fmt.Sprintf("SysUsersPkg: %q\n", p.SysUsersPkg)) + return sb.String() +} + +func (p Params) Validate() error { + if !gno.ReRealmPath.MatchString(p.SysUsersPkg) { + return fmt.Errorf("invalid package/realm path %q, failed to match %q", p.SysUsersPkg, gno.ReRealmPath) + } + return nil +} + +// Equals returns a boolean determining if two Params types are identical. +func (p Params) Equals(p2 Params) bool { + return amino.DeepEqual(p, p2) +} + +func (vm *VMKeeper) SetParams(ctx sdk.Context, params Params) error { + if err := params.Validate(); err != nil { + return err + } + err := vm.prmk.SetParams(ctx, ModuleName, paramsKey, params) + return err +} + +func (vm *VMKeeper) GetParams(ctx sdk.Context) Params { + params := &Params{} + + _, err := vm.prmk.GetParams(ctx, ModuleName, paramsKey, params) + if err != nil { + panic(err.Error()) + } + + return *params +} const ( sysUsersPkgParamPath = "gno.land/r/sys/params.sys.users_pkgpath.string" - chainDomainParamPath = "gno.land/r/sys/params.chain_domain.string" + chainDomainParamPath = "gno.land/r/sys/params.vm.chain_domain.string" ) func (vm *VMKeeper) getChainDomainParam(ctx sdk.Context) string { @@ -18,3 +86,12 @@ func (vm *VMKeeper) getSysUsersPkgParam(ctx sdk.Context) string { vm.prmk.GetString(ctx, sysUsersPkgParamPath, &sysUsersPkg) return sysUsersPkg } + +func (vm *VMKeeper) GetParamfulKey() string { + return ModuleName +} + +// WillSetParam checks if the key contains the module's parameter key prefix and updates the module parameter accordingly. +func (vm *VMKeeper) WillSetParam(ctx sdk.Context, key string, value interface{}) { + // TODO: move parameters here. +} diff --git a/gnovm/stdlibs/std/params.go b/gnovm/stdlibs/std/params.go index e21bd9912dd..00acb4d795e 100644 --- a/gnovm/stdlibs/std/params.go +++ b/gnovm/stdlibs/std/params.go @@ -66,7 +66,7 @@ func pkey(m *gno.Machine, key string, kind string) string { } } - // decorate key with realm and type. _, rlmPath := currentRealm(m) + // decorate key with realm and type. return fmt.Sprintf("%s.%s", rlmPath, key) } diff --git a/tm2/pkg/sdk/auth/abci.go b/tm2/pkg/sdk/auth/abci.go index 86cbf962fad..9cbebe676e9 100644 --- a/tm2/pkg/sdk/auth/abci.go +++ b/tm2/pkg/sdk/auth/abci.go @@ -14,6 +14,6 @@ func EndBlocker(ctx sdk.Context, gk GasPriceKeeperI) { // InitChainer is called in the InitChain(), it set the initial gas price in the // GasPriceKeeper store // for the next gas price -func InitChainer(ctx sdk.Context, gk GasPriceKeeper, gp std.GasPrice) { +func InitChainer(ctx sdk.Context, gk GasPriceKeeperI, gp std.GasPrice) { gk.SetGasPrice(ctx, gp) } diff --git a/tm2/pkg/sdk/auth/ante.go b/tm2/pkg/sdk/auth/ante.go index f941f398b17..59582de0bc7 100644 --- a/tm2/pkg/sdk/auth/ante.go +++ b/tm2/pkg/sdk/auth/ante.go @@ -322,7 +322,8 @@ func DeductFees(bank BankKeeperI, ctx sdk.Context, acc std.Account, fees std.Coi )) } - err := bank.SendCoins(ctx, acc.GetAddress(), FeeCollectorAddress(), fees) + // Sending coins is unrestricted to pay for gas fees + err := bank.SendCoinsUnrestricted(ctx, acc.GetAddress(), FeeCollectorAddress(), fees) if err != nil { return abciResult(err) } diff --git a/tm2/pkg/sdk/auth/consts.go b/tm2/pkg/sdk/auth/consts.go index 462ca0cd64d..95afb226586 100644 --- a/tm2/pkg/sdk/auth/consts.go +++ b/tm2/pkg/sdk/auth/consts.go @@ -23,6 +23,9 @@ const ( GasPriceKey = "gasPrice" // param key for global account number GlobalAccountNumberKey = "globalAccountNumber" + + // param + ParamsKeyPrefix = ModuleName ) // AddressStoreKey turn an address to key used to get it from the account store diff --git a/tm2/pkg/sdk/auth/genesis.go b/tm2/pkg/sdk/auth/genesis.go index c863c237a41..f5d6d97264e 100644 --- a/tm2/pkg/sdk/auth/genesis.go +++ b/tm2/pkg/sdk/auth/genesis.go @@ -5,6 +5,27 @@ import ( "github.com/gnolang/gno/tm2/pkg/sdk" ) +// GenesisState - all state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params" yaml:"params"` +} + +// NewGenesisState - Create a new genesis state +func NewGenesisState(params Params) GenesisState { + return GenesisState{params} +} + +// DefaultGenesisState - Return a default genesis state +func DefaultGenesisState() GenesisState { + return NewGenesisState(DefaultParams()) +} + +// ValidateGenesis performs basic validation of genesis data returning an +// error for any failed validation criteria. +func ValidateGenesis(data GenesisState) error { + return data.Params.Validate() +} + // InitGenesis - Init store state from genesis data func (ak AccountKeeper) InitGenesis(ctx sdk.Context, data GenesisState) { if amino.DeepEqual(data, GenesisState{}) { diff --git a/tm2/pkg/sdk/auth/keeper.go b/tm2/pkg/sdk/auth/keeper.go index fc83997fdc4..741c8e8d287 100644 --- a/tm2/pkg/sdk/auth/keeper.go +++ b/tm2/pkg/sdk/auth/keeper.go @@ -17,8 +17,10 @@ import ( type AccountKeeper struct { // The (unexposed) key used to access the store from the Context. key store.StoreKey - // The keeper used to store auth parameters + + // store module parameters paramk params.ParamsKeeper + // The prototypical Account constructor. proto func() std.Account } diff --git a/tm2/pkg/sdk/auth/params.go b/tm2/pkg/sdk/auth/params.go index fda85c7a3d6..503716c2772 100644 --- a/tm2/pkg/sdk/auth/params.go +++ b/tm2/pkg/sdk/auth/params.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -13,25 +14,29 @@ type AuthParamsContextKey struct{} // Default parameter values const ( - DefaultMaxMemoBytes int64 = 65536 - DefaultTxSigLimit int64 = 7 - DefaultTxSizeCostPerByte int64 = 10 - DefaultSigVerifyCostED25519 int64 = 590 - DefaultSigVerifyCostSecp256k1 int64 = 1000 + DefaultMaxMemoBytes int64 = 65536 + DefaultTxSigLimit int64 = 7 + DefaultTxSizeCostPerByte int64 = 10 + DefaultSigVerifyCostED25519 int64 = 590 + DefaultSigVerifyCostSecp256k1 int64 = 1000 + DefaultGasPricesChangeCompressor int64 = 10 DefaultTargetGasRatio int64 = 70 // 70% of the MaxGas in a block + + paramsKey = "p" ) // Params defines the parameters for the auth module. type Params struct { - MaxMemoBytes int64 `json:"max_memo_bytes" yaml:"max_memo_bytes"` - TxSigLimit int64 `json:"tx_sig_limit" yaml:"tx_sig_limit"` - TxSizeCostPerByte int64 `json:"tx_size_cost_per_byte" yaml:"tx_size_cost_per_byte"` - SigVerifyCostED25519 int64 `json:"sig_verify_cost_ed25519" yaml:"sig_verify_cost_ed25519"` - SigVerifyCostSecp256k1 int64 `json:"sig_verify_cost_secp256k1" yaml:"sig_verify_cost_secp256k1"` - GasPricesChangeCompressor int64 `json:"gas_price_change_compressor" yaml:"gas_price_change_compressor"` - TargetGasRatio int64 `json:"target_gas_ratio" yaml:"target_gas_ratio"` - InitialGasPrice std.GasPrice `json:"initial_gasprice"` + MaxMemoBytes int64 `json:"max_memo_bytes" yaml:"max_memo_bytes"` + TxSigLimit int64 `json:"tx_sig_limit" yaml:"tx_sig_limit"` + TxSizeCostPerByte int64 `json:"tx_size_cost_per_byte" yaml:"tx_size_cost_per_byte"` + SigVerifyCostED25519 int64 `json:"sig_verify_cost_ed25519" yaml:"sig_verify_cost_ed25519"` + SigVerifyCostSecp256k1 int64 `json:"sig_verify_cost_secp256k1" yaml:"sig_verify_cost_secp256k1"` + GasPricesChangeCompressor int64 `json:"gas_price_change_compressor" yaml:"gas_price_change_compressor"` + TargetGasRatio int64 `json:"target_gas_ratio" yaml:"target_gas_ratio"` + InitialGasPrice std.GasPrice `json:"initial_gasprice"` + UnrestrictedAddrs []crypto.Address `json:"unrestricted_addrs" yaml:"unrestricted_addrs"` } // NewParams creates a new Params object @@ -56,15 +61,15 @@ func (p Params) Equals(p2 Params) bool { // DefaultParams returns a default set of parameters. func DefaultParams() Params { - return Params{ - MaxMemoBytes: DefaultMaxMemoBytes, - TxSigLimit: DefaultTxSigLimit, - TxSizeCostPerByte: DefaultTxSizeCostPerByte, - SigVerifyCostED25519: DefaultSigVerifyCostED25519, - SigVerifyCostSecp256k1: DefaultSigVerifyCostSecp256k1, - GasPricesChangeCompressor: DefaultGasPricesChangeCompressor, - TargetGasRatio: DefaultTargetGasRatio, - } + return NewParams( + DefaultMaxMemoBytes, + DefaultTxSigLimit, + DefaultTxSizeCostPerByte, + DefaultSigVerifyCostED25519, + DefaultSigVerifyCostSecp256k1, + DefaultGasPricesChangeCompressor, + DefaultTargetGasRatio, + ) } // String implements the stringer interface. @@ -83,16 +88,19 @@ func (p Params) String() string { } func (p Params) Validate() error { - if p.TxSigLimit == 0 { + if p.MaxMemoBytes <= 0 { + return fmt.Errorf("invalid max memo bytes: %d", p.MaxMemoBytes) + } + if p.TxSigLimit <= 0 { return fmt.Errorf("invalid tx signature limit: %d", p.TxSigLimit) } - if p.SigVerifyCostED25519 == 0 { + if p.SigVerifyCostED25519 <= 0 { return fmt.Errorf("invalid ED25519 signature verification cost: %d", p.SigVerifyCostED25519) } - if p.SigVerifyCostSecp256k1 == 0 { + if p.SigVerifyCostSecp256k1 <= 0 { return fmt.Errorf("invalid SECK256k1 signature verification cost: %d", p.SigVerifyCostSecp256k1) } - if p.TxSizeCostPerByte == 0 { + if p.TxSizeCostPerByte <= 0 { return fmt.Errorf("invalid tx size cost per byte: %d", p.TxSizeCostPerByte) } if p.GasPricesChangeCompressor <= 0 { @@ -108,20 +116,26 @@ func (ak AccountKeeper) SetParams(ctx sdk.Context, params Params) error { if err := params.Validate(); err != nil { return err } - err := ak.paramk.SetParams(ctx, ModuleName, params) + err := ak.paramk.SetParams(ctx, ModuleName, paramsKey, params) return err } func (ak AccountKeeper) GetParams(ctx sdk.Context) Params { params := &Params{} - ok, err := ak.paramk.GetParams(ctx, ModuleName, params) - - if !ok { - panic("params key " + ModuleName + " does not exist") - } + _, err := ak.paramk.GetParams(ctx, ModuleName, paramsKey, params) if err != nil { panic(err.Error()) } + return *params } + +func (ak AccountKeeper) GetParamfulKey() string { + return ModuleName +} + +// WillSetParam checks if the key contains the module's parameter key prefix and updates the module parameter accordingly. +func (ak AccountKeeper) WillSetParam(ctx sdk.Context, key string, value interface{}) { + // TODO: add parameter settings here. +} diff --git a/tm2/pkg/sdk/auth/test_common.go b/tm2/pkg/sdk/auth/test_common.go index e0a6316bead..7d0d031c914 100644 --- a/tm2/pkg/sdk/auth/test_common.go +++ b/tm2/pkg/sdk/auth/test_common.go @@ -28,12 +28,15 @@ func setupTestEnv() testEnv { ms := store.NewCommitMultiStore(db) ms.MountStoreWithDB(authCapKey, iavl.StoreConstructor, db) ms.LoadLatestVersion() + paramk := params.NewParamsKeeper(authCapKey) - paramk := params.NewParamsKeeper(authCapKey, "") acck := NewAccountKeeper(authCapKey, paramk, std.ProtoBaseAccount) bank := NewDummyBankKeeper(acck) gk := NewGasPriceKeeper(authCapKey) + paramk.Register(acck.GetParamfulKey(), acck) + paramk.Register(bank.GetParamfulKey(), bank) + ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{Height: 1, ChainID: "test-chain-id"}, log.NewNoopLogger()) ctx = ctx.WithValue(AuthParamsContextKey{}, DefaultParams()) ctx = ctx.WithConsensusParams(&abci.ConsensusParams{ @@ -63,6 +66,10 @@ func NewDummyBankKeeper(acck AccountKeeper) DummyBankKeeper { return DummyBankKeeper{acck} } +func (bank DummyBankKeeper) SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + return bank.SendCoins(ctx, fromAddr, toAddr, amt) +} + // SendCoins for the dummy supply keeper func (bank DummyBankKeeper) SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { fromAcc := bank.acck.GetAccount(ctx, fromAddr) @@ -87,3 +94,10 @@ func (bank DummyBankKeeper) SendCoins(ctx sdk.Context, fromAddr crypto.Address, return nil } + +func (bank DummyBankKeeper) GetParamfulKey() string { + return "dummy_bank" +} + +// WillSetParam checks if the key contains the module's parameter key prefix and updates the module parameter accordingly. +func (bank DummyBankKeeper) WillSetParam(ctx sdk.Context, key string, value interface{}) { return } diff --git a/tm2/pkg/sdk/auth/types.go b/tm2/pkg/sdk/auth/types.go index 3fb2d10fbb5..4965122bf67 100644 --- a/tm2/pkg/sdk/auth/types.go +++ b/tm2/pkg/sdk/auth/types.go @@ -22,6 +22,7 @@ var _ AccountKeeperI = AccountKeeper{} // Limited interface only needed for auth. type BankKeeperI interface { SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error + SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error } type GasPriceKeeperI interface { @@ -31,24 +32,3 @@ type GasPriceKeeperI interface { } var _ GasPriceKeeperI = GasPriceKeeper{} - -// GenesisState - all auth state that must be provided at genesis -type GenesisState struct { - Params Params `json:"params"` -} - -// NewGenesisState - Create a new genesis state -func NewGenesisState(params Params) GenesisState { - return GenesisState{params} -} - -// DefaultGenesisState - Return a default genesis state -func DefaultGenesisState() GenesisState { - return NewGenesisState(DefaultParams()) -} - -// ValidateGenesis performs basic validation of auth genesis data returning an -// error for any failed validation criteria. -func ValidateGenesis(data GenesisState) error { - return data.Params.Validate() -} diff --git a/tm2/pkg/sdk/bank/common_test.go b/tm2/pkg/sdk/bank/common_test.go index c8210be7175..28d0b72c2c4 100644 --- a/tm2/pkg/sdk/bank/common_test.go +++ b/tm2/pkg/sdk/bank/common_test.go @@ -16,9 +16,10 @@ import ( ) type testEnv struct { - ctx sdk.Context - bank BankKeeper - acck auth.AccountKeeper + ctx sdk.Context + bank BankKeeper + acck auth.AccountKeeper + paramk params.ParamsKeeper } func setupTestEnv() testEnv { @@ -29,13 +30,16 @@ func setupTestEnv() testEnv { ms := store.NewCommitMultiStore(db) ms.MountStoreWithDB(authCapKey, iavl.StoreConstructor, db) ms.LoadLatestVersion() - paramk := params.NewParamsKeeper(authCapKey, "") ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) + + paramk := params.NewParamsKeeper(authCapKey) acck := auth.NewAccountKeeper( authCapKey, paramk, std.ProtoBaseAccount, ) + bank := NewBankKeeper(acck, paramk) - bank := NewBankKeeper(acck) + paramk.Register(acck.GetParamfulKey(), acck) + paramk.Register(bank.GetParamfulKey(), bank) - return testEnv{ctx: ctx, bank: bank, acck: acck} + return testEnv{ctx: ctx, bank: bank, acck: acck, paramk: paramk} } diff --git a/tm2/pkg/sdk/bank/consts.go b/tm2/pkg/sdk/bank/consts.go index 4284a44c940..7abeb11e392 100644 --- a/tm2/pkg/sdk/bank/consts.go +++ b/tm2/pkg/sdk/bank/consts.go @@ -1,5 +1,7 @@ package bank const ( - ModuleName = "bank" + ModuleName = "bank" + ParamsKeyPrefix = ModuleName + lockSendKey = "lockSend.string" ) diff --git a/tm2/pkg/sdk/bank/genesis.go b/tm2/pkg/sdk/bank/genesis.go new file mode 100644 index 00000000000..3247718655a --- /dev/null +++ b/tm2/pkg/sdk/bank/genesis.go @@ -0,0 +1,50 @@ +package bank + +import ( + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/sdk" +) + +// GenesisState - all state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params" yaml:"params"` +} + +// NewGenesisState - Create a new genesis state +func NewGenesisState(params Params) GenesisState { + return GenesisState{params} +} + +// DefaultGenesisState - Return a default genesis state +func DefaultGenesisState() GenesisState { + return NewGenesisState(DefaultParams()) +} + +// ValidateGenesis performs basic validation of genesis data returning an +// error for any failed validation criteria. +func ValidateGenesis(data GenesisState) error { + return data.Params.Validate() +} + +// InitGenesis - Init store state from genesis data +func (bank BankKeeper) InitGenesis(ctx sdk.Context, data GenesisState) { + if amino.DeepEqual(data, GenesisState{}) { + if err := bank.SetParams(ctx, DefaultParams()); err != nil { + panic(err) + } + } + if err := ValidateGenesis(data); err != nil { + panic(err) + } + + if err := bank.SetParams(ctx, data.Params); err != nil { + panic(err) + } +} + +// ExportGenesis returns a GenesisState for a given context and keeper +func (bank BankKeeper) ExportGenesis(ctx sdk.Context) GenesisState { + params := bank.GetParams(ctx) + + return NewGenesisState(params) +} diff --git a/tm2/pkg/sdk/bank/keeper.go b/tm2/pkg/sdk/bank/keeper.go index f98e6b3e225..7a7aaaec88c 100644 --- a/tm2/pkg/sdk/bank/keeper.go +++ b/tm2/pkg/sdk/bank/keeper.go @@ -7,6 +7,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/params" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -21,9 +22,13 @@ type BankKeeperI interface { SubtractCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) AddCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) SetCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) error + SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error + + InitGenesis(ctx sdk.Context, data GenesisState) + GetParams(ctx sdk.Context) Params } -var _ BankKeeperI = BankKeeper{} +var _ BankKeeperI = &BankKeeper{} // BankKeeper only allows transfers between accounts without the possibility of // creating coins. It implements the BankKeeperI interface. @@ -31,16 +36,82 @@ type BankKeeper struct { ViewKeeper acck auth.AccountKeeper + // The keeper used to store parameters + paramk params.ParamsKeeper } // NewBankKeeper returns a new BankKeeper. -func NewBankKeeper(acck auth.AccountKeeper) BankKeeper { +func NewBankKeeper(acck auth.AccountKeeper, pk params.ParamsKeeper) BankKeeper { return BankKeeper{ ViewKeeper: NewViewKeeper(acck), acck: acck, + paramk: pk, + } +} + +func (bank BankKeeper) AddRestrictedDenoms(ctx sdk.Context, restrictedDenoms ...string) { + if len(restrictedDenoms) == 0 { + return + } + params := bank.GetParams(ctx) + rdSet := toSet(params.RestrictedDenoms) + for _, denom := range restrictedDenoms { + rdSet[denom] = struct{}{} + } + + ds := make([]string, 0, len(rdSet)) + for d := range rdSet { + ds = append(ds, d) + } + params.RestrictedDenoms = ds + if err := bank.SetParams(ctx, params); err != nil { + panic(err) + } +} + +func (bank BankKeeper) DelRestrictedDenoms(ctx sdk.Context, restrictedDenoms ...string) { + params := bank.GetParams(ctx) + + rdSet := toSet(params.RestrictedDenoms) + for _, denom := range restrictedDenoms { + delete(rdSet, denom) + } + ds := make([]string, 0, len(rdSet)) + for d := range rdSet { + ds = append(ds, d) + } + + if err := bank.SetParams(ctx, params); err != nil { + panic(err) + } +} + +func (bank BankKeeper) DelAllRestrictedDenoms(ctx sdk.Context) { + params := bank.GetParams(ctx) + params.RestrictedDenoms = []string{} + + err := bank.paramk.SetParams(ctx, ModuleName, paramsKey, params) + if err != nil { + panic(err) } } +func (bank BankKeeper) RestrictedDenoms(ctx sdk.Context) []string { + params := bank.GetParams(ctx) + return params.RestrictedDenoms +} + +type stringSet map[string]struct{} + +func toSet(str []string) stringSet { + ss := stringSet{} + + for _, key := range str { + ss[key] = struct{}{} + } + return ss +} + // InputOutputCoins handles a list of inputs and outputs func (bank BankKeeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) error { // Safety check ensuring that when sending coins the bank must maintain the @@ -50,6 +121,9 @@ func (bank BankKeeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs } for _, in := range inputs { + if !bank.canSendCoins(ctx, in.Address, in.Coins) { + return std.RestrictedTransferError{} + } _, err := bank.SubtractCoins(ctx, in.Address, in.Coins) if err != nil { return err @@ -84,8 +158,45 @@ func (bank BankKeeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs return nil } -// SendCoins moves coins from one account to another +// canSendCoins returns true if the coins can be sent without violating any restriction. +func (bank BankKeeper) canSendCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) bool { + rds := bank.RestrictedDenoms(ctx) + if len(rds) == 0 { + // No restrictions. + return true + } + if amt.ContainOneOfDenom(toSet(rds)) { + acc := bank.acck.GetAccount(ctx, addr) + accr := acc.(std.AccountRestricter) + if acc != nil && accr.IsRestricted() { + return false + } + } + return true +} + +// SendCoins moves coins from one account to another, restrction could be applied func (bank BankKeeper) SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + // read restricted boolean value from param.IsRestrictedTransfer() + // canSendCoins is true until they have agreed to the waiver + if !bank.canSendCoins(ctx, fromAddr, amt) { + return std.RestrictedTransferError{} + } + + return bank.sendCoins(ctx, fromAddr, toAddr, amt) +} + +// SendCoinsUnrestricted is used for paying gas. +func (bank BankKeeper) SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + return bank.sendCoins(ctx, fromAddr, toAddr, amt) +} + +func (bank BankKeeper) sendCoins( + ctx sdk.Context, + fromAddr crypto.Address, + toAddr crypto.Address, + amt std.Coins, +) error { _, err := bank.SubtractCoins(ctx, fromAddr, amt) if err != nil { return err diff --git a/tm2/pkg/sdk/bank/keeper_test.go b/tm2/pkg/sdk/bank/keeper_test.go index df2039a682c..e707b22c29d 100644 --- a/tm2/pkg/sdk/bank/keeper_test.go +++ b/tm2/pkg/sdk/bank/keeper_test.go @@ -95,7 +95,7 @@ func TestBankKeeper(t *testing.T) { env := setupTestEnv() ctx := env.ctx - bank := NewBankKeeper(env.acck) + bank := env.bank addr := crypto.AddressFromPreimage([]byte("addr1")) addr2 := crypto.AddressFromPreimage([]byte("addr2")) diff --git a/tm2/pkg/sdk/bank/params.go b/tm2/pkg/sdk/bank/params.go new file mode 100644 index 00000000000..3b83c27789c --- /dev/null +++ b/tm2/pkg/sdk/bank/params.go @@ -0,0 +1,84 @@ +package bank + +import ( + "fmt" + "strings" + + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/std" +) + +const paramsKey = "p" + +type BankParamsContextKey struct{} + +// Params defines the parameters for the bank module. +type Params struct { + RestrictedDenoms []string `json:"restricted_denoms" yaml:"restricted_denoms"` +} + +// NewParams creates a new Params object +func NewParams(restDenoms []string) Params { + return Params{ + RestrictedDenoms: restDenoms, + } +} + +// DefaultParams returns a default set of parameters. +func DefaultParams() Params { + return NewParams([]string{}) +} + +// String implements the stringer interface. +func (p Params) String() string { + var sb strings.Builder + sb.WriteString("Params: \n") + sb.WriteString(fmt.Sprintf("RestrictedDenom: %q\n", p.RestrictedDenoms)) + return sb.String() +} + +func (p *Params) Validate() error { + for _, denom := range p.RestrictedDenoms { + err := std.ValidateDenom(denom) + if err != nil { + return fmt.Errorf("invalid restricted denom: %s", denom) + } + } + return nil +} + +func (bank BankKeeper) SetParams(ctx sdk.Context, params Params) error { + if len(params.RestrictedDenoms) == 0 { + return nil + } + if err := params.Validate(); err != nil { + return err + } + err := bank.paramk.SetParams(ctx, ModuleName, paramsKey, params) + + return err +} + +func (bank BankKeeper) GetParams(ctx sdk.Context) Params { + params := &Params{} + _, err := bank.paramk.GetParams(ctx, ModuleName, paramsKey, params) + if err != nil { + panic(err.Error()) + } + return *params +} + +func (bank BankKeeper) GetParamfulKey() string { + return ModuleName +} + +// WillSetParam checks if the key contains the module's parameter key and updates the module parameter accordingly. +func (bank BankKeeper) WillSetParam(ctx sdk.Context, key string, value interface{}) { + if key == lockSendKey { + if value != "" { // lock sending denoms + bank.AddRestrictedDenoms(ctx, value.(string)) + } else { // unlock sending ugnot + bank.DelAllRestrictedDenoms(ctx) + } + } +} diff --git a/tm2/pkg/sdk/params/handler.go b/tm2/pkg/sdk/params/handler.go index b662fc06c58..a74cdc36ee0 100644 --- a/tm2/pkg/sdk/params/handler.go +++ b/tm2/pkg/sdk/params/handler.go @@ -28,14 +28,13 @@ func (bh paramsHandler) Process(ctx sdk.Context, msg std.Msg) sdk.Result { // Query func (bh paramsHandler) Query(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { - switch secondPart(req.Path) { - case bh.params.prefix: + prefix := secondPart(req.Path) + if bh.params.PrefixExists(prefix) { return bh.queryParam(ctx, req) - default: - res = sdk.ABCIResponseQueryFromError( - std.ErrUnknownRequest("unknown params query endpoint")) - return } + res = sdk.ABCIResponseQueryFromError( + std.ErrUnknownRequest("unknown params query endpoint")) + return } // queryParam returns param for a key. diff --git a/tm2/pkg/sdk/params/keeper.go b/tm2/pkg/sdk/params/keeper.go index c99b9dbfde1..16867b25125 100644 --- a/tm2/pkg/sdk/params/keeper.go +++ b/tm2/pkg/sdk/params/keeper.go @@ -1,6 +1,7 @@ package params import ( + "fmt" "log/slog" "strings" @@ -37,47 +38,52 @@ type ParamsKeeperI interface { Has(ctx sdk.Context, key string) bool GetRaw(ctx sdk.Context, key string) []byte + GetParams(ctx sdk.Context, prefixKey string, key string, target interface{}) (bool, error) + SetParams(ctx sdk.Context, prefixKey string, key string, params interface{}) error + // XXX: ListKeys? } +type ParamfulKeeper interface { + GetParamfulKey() string + WillSetParam(ctx sdk.Context, key string, value interface{}) +} var _ ParamsKeeperI = ParamsKeeper{} // global paramstore Keeper. type ParamsKeeper struct { - key store.StoreKey - prefix string + key store.StoreKey + kprs map[string]ParamfulKeeper // Register a prefix for module parameter keys. } // NewParamsKeeper returns a new ParamsKeeper. -func NewParamsKeeper(key store.StoreKey, prefix string) ParamsKeeper { +func NewParamsKeeper(key store.StoreKey) ParamsKeeper { return ParamsKeeper{ - key: key, - prefix: prefix, + key: key, + kprs: map[string]ParamfulKeeper{}, } } -// GetParam gets a param value from the global param store. -func (pk ParamsKeeper) GetParams(ctx sdk.Context, key string, target interface{}) (bool, error) { - stor := ctx.Store(pk.key) +func (pk ParamsKeeper) GetRegisteredKeeper(keeperKey string) ParamfulKeeper { + rk, ok := pk.kprs[keeperKey] - bz := stor.Get(ValueStoreKey(key)) - if bz == nil { - return false, nil + if !ok { + panic("keeper key " + keeperKey + " does not exist") } + return rk +} - return true, amino.UnmarshalJSON(bz, target) +func (pk ParamsKeeper) Register(keeperKey string, pmk ParamfulKeeper) { + pk.kprs[keeperKey] = pmk } -// SetParam sets a param value to the global param store. -func (pk ParamsKeeper) SetParams(ctx sdk.Context, key string, param interface{}) error { - stor := ctx.Store(pk.key) - bz, err := amino.MarshalJSON(param) - if err != nil { - return err - } +func (pk ParamsKeeper) IsRegistered(keeperKey string) bool { + _, ok := pk.kprs[keeperKey] + return ok +} - stor.Set(ValueStoreKey(key), bz) - return nil +func (pk ParamsKeeper) PrefixExists(prefix string) bool { + return pk.IsRegistered(prefix) } // XXX: why do we expose this? @@ -145,6 +151,47 @@ func (pk ParamsKeeper) SetBytes(ctx sdk.Context, key string, value []byte) { pk.set(ctx, key, value) } +// GetParam gets a param value from the global param store. +func (pk ParamsKeeper) GetParams(ctx sdk.Context, moduleKey string, key string, target interface{}) (bool, error) { + if moduleKey != "" { + if pk.IsRegistered(moduleKey) { + key = moduleKey + "_" + key + } else { + return false, fmt.Errorf("params module key %q does not exisit", moduleKey) + } + } + + stor := ctx.Store(pk.key) + vk := ValueStoreKey(key) + bz := stor.Get(vk) + if bz == nil { + return false, nil + } + + return true, amino.UnmarshalJSON(bz, target) +} + +// SetParam sets a param value to the global param store. +func (pk ParamsKeeper) SetParams(ctx sdk.Context, moduleKey string, key string, param interface{}) error { + if moduleKey != "" { + if pk.IsRegistered(moduleKey) { + key = moduleKey + "_" + key + } else { + return fmt.Errorf("parameter module key %q does not exist", moduleKey) + } + } + + bz, err := amino.MarshalJSON(param) + if err != nil { + return err + } + + stor := ctx.Store(pk.key) + vk := ValueStoreKey(key) + stor.Set(vk, bz) + return nil +} + func (pk ParamsKeeper) getIfExists(ctx sdk.Context, key string, ptr interface{}) { stor := ctx.Store(pk.key) bz := stor.Get([]byte(key)) diff --git a/tm2/pkg/sdk/params/keeper_test.go b/tm2/pkg/sdk/params/keeper_test.go index aedfaa9d5a3..80224028764 100644 --- a/tm2/pkg/sdk/params/keeper_test.go +++ b/tm2/pkg/sdk/params/keeper_test.go @@ -152,12 +152,12 @@ func TestGetAndSetParams(t *testing.T) { keeper := env.keeper // SetParams a := Params{p1: 1, p2: "a"} - err := keeper.SetParams(ctx, ModuleName, a) + err := keeper.SetParams(ctx, "", "p", a) require.NoError(t, err) // GetParams a1 := Params{} - _, err1 := keeper.GetParams(ctx, ModuleName, &a1) + _, err1 := keeper.GetParams(ctx, "", "p", &a1) require.NoError(t, err1) require.True(t, amino.DeepEqual(a, a1), "a and a1 should equal") } diff --git a/tm2/pkg/sdk/params/params_test.go b/tm2/pkg/sdk/params/params_test.go new file mode 100644 index 00000000000..8f20a13c5ad --- /dev/null +++ b/tm2/pkg/sdk/params/params_test.go @@ -0,0 +1,63 @@ +package params + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/stretchr/testify/require" +) + +// NewModuleParams defines the parameters with updated fields for a module. +type NewModuleParams struct { + LimitedTokens []string `json:"limited_tokens" yaml:"limited_tokens"` + Max uint64 `json:"max" yaml:"max"` +} + +// OldModuleParams defines the parameters for a module. +type OldModuleParams struct { + LimitedTokens []string `json:"limited_tokens" yaml:"limited_tokens"` +} + +// NewOldModuleParams creates a new OldModuleParams object. +func NewOldModuleParams(tokens []string) OldModuleParams { + return OldModuleParams{ + LimitedTokens: tokens, + } +} + +func TestBackwardCompatibility(t *testing.T) { + oldParams := NewOldModuleParams([]string{"token1", "token2"}) + + // Serialize OldModuleParams to JSON + bz, err := amino.MarshalJSON(oldParams) + require.NoError(t, err, "Failed to marshal OldModuleParams") + + t.Logf("Serialized OldModuleParams: %s\n", bz) + + // Deserialize JSON into NewModuleParams + newParams := &NewModuleParams{} + err = amino.UnmarshalJSON(bz, newParams) + require.NoError(t, err, "Failed to unmarshal into NewModuleParams") + + // Validate compatibility + require.Equal(t, oldParams.LimitedTokens, newParams.LimitedTokens, "LimitedTokens mismatch") + require.Equal(t, uint64(0), newParams.Max, "Max should default to 0 in backward compatibility") +} + +func TestForwardCompatibility(t *testing.T) { + newParams := NewModuleParams{LimitedTokens: []string{"token1", "token2"}, Max: 10} + + // Serialize NewModuleParams to JSON + bz, err := amino.MarshalJSON(newParams) + require.NoError(t, err, "Failed to marshal NewModuleParams") + + t.Logf("Serialized NewModuleParams: %s\n", bz) + + // Deserialize JSON into OldModuleParams + oldParams := &OldModuleParams{} + err = amino.UnmarshalJSON(bz, oldParams) + require.NoError(t, err, "Failed to unmarshal into OldModuleParams") + + // Validate compatibility + require.Equal(t, newParams.LimitedTokens, oldParams.LimitedTokens, "LimitedTokens mismatch") +} diff --git a/tm2/pkg/sdk/params/test_common.go b/tm2/pkg/sdk/params/test_common.go index 8243ee867de..97a1016d002 100644 --- a/tm2/pkg/sdk/params/test_common.go +++ b/tm2/pkg/sdk/params/test_common.go @@ -6,6 +6,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/iavl" ) @@ -23,8 +24,9 @@ func setupTestEnv() testEnv { ms.MountStoreWithDB(paramsCapKey, iavl.StoreConstructor, db) ms.LoadLatestVersion() - prefix := "params_test" - keeper := NewParamsKeeper(paramsCapKey, prefix) + paramk := NewParamsKeeper(paramsCapKey) + dk := NewDummyKeeper() + paramk.Register(dk.GetParamfulKey(), dk) ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{Height: 1, ChainID: "test-chain-id"}, log.NewNoopLogger()) // XXX: context key? @@ -42,5 +44,18 @@ func setupTestEnv() testEnv { }) stor := ctx.Store(paramsCapKey) - return testEnv{ctx: ctx, store: stor, keeper: keeper} + return testEnv{ctx: ctx, store: stor, keeper: paramk} +} + +const DummyModuleName = "params_test" + +type DummyKeeper struct{} + +func NewDummyKeeper() DummyKeeper { + return DummyKeeper{} +} + +func (dk DummyKeeper) GetParamfulKey() string { + return DummyModuleName } +func (dk DummyKeeper) WillSetParam(ctx sdk.Context, key string, value interface{}) {} diff --git a/tm2/pkg/std/account.go b/tm2/pkg/std/account.go index c70f43d22e9..ff8d0c7a724 100644 --- a/tm2/pkg/std/account.go +++ b/tm2/pkg/std/account.go @@ -19,6 +19,7 @@ import ( // // Many complex conditions can be used in the concrete struct which implements Account. type Account interface { + AccountRestricter GetAddress() crypto.Address SetAddress(crypto.Address) error // errors if already set. @@ -151,3 +152,11 @@ func (acc *BaseAccount) SetSequence(seq uint64) error { acc.Sequence = seq return nil } + +type AccountRestricter interface { + IsRestricted() bool +} + +func (acc *BaseAccount) IsRestricted() bool { + return false +} diff --git a/tm2/pkg/std/coin.go b/tm2/pkg/std/coin.go index fba20a5ba78..118567ffe40 100644 --- a/tm2/pkg/std/coin.go +++ b/tm2/pkg/std/coin.go @@ -63,7 +63,7 @@ func (coin Coin) String() string { // validate returns an error if the Coin has a negative amount or if // the denom is invalid. func validate(denom string, amount int64) error { - if err := validateDenom(denom); err != nil { + if err := ValidateDenom(denom); err != nil { return err } @@ -229,7 +229,7 @@ func (coins Coins) IsValid() bool { case 0: return true case 1: - if err := validateDenom(coins[0].Denom); err != nil { + if err := ValidateDenom(coins[0].Denom); err != nil { return false } return coins[0].IsPositive() @@ -328,6 +328,21 @@ func (coins Coins) AddUnsafe(coinsB Coins) Coins { } } +// ContainOneOfDenom check if a Coins instance contains a denom in the provided denomos +func (coins Coins) ContainOneOfDenom(denoms map[string]struct{}) bool { + if len(denoms) == 0 { + return false + } + + for _, coin := range coins { + if _, ok := denoms[coin.Denom]; ok { + return true + } + } + + return false +} + // DenomsSubsetOf returns true if receiver's denom set // is subset of coinsB's denoms. func (coins Coins) DenomsSubsetOf(coinsB Coins) bool { @@ -623,7 +638,7 @@ var ( reCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, reDnmString)) ) -func validateDenom(denom string) error { +func ValidateDenom(denom string) error { if !reDnm.MatchString(denom) { return fmt.Errorf("invalid denom: %s", denom) } @@ -631,7 +646,7 @@ func validateDenom(denom string) error { } func mustValidateDenom(denom string) { - if err := validateDenom(denom); err != nil { + if err := ValidateDenom(denom); err != nil { panic(err) } } @@ -661,7 +676,7 @@ func ParseCoin(coinStr string) (coin Coin, err error) { return Coin{}, errors.Wrapf(err, "failed to parse coin amount: %s", amountStr) } - if err := validateDenom(denomStr); err != nil { + if err := ValidateDenom(denomStr); err != nil { return Coin{}, fmt.Errorf("invalid denom cannot contain upper case characters or spaces: %w", err) } diff --git a/tm2/pkg/std/errors.go b/tm2/pkg/std/errors.go index 715b27b3eb4..48bd93be1de 100644 --- a/tm2/pkg/std/errors.go +++ b/tm2/pkg/std/errors.go @@ -14,43 +14,45 @@ func (abciError) AssertABCIError() {} type InternalError struct{ abciError } type ( - TxDecodeError struct{ abciError } - InvalidSequenceError struct{ abciError } - UnauthorizedError struct{ abciError } - InsufficientFundsError struct{ abciError } - UnknownRequestError struct{ abciError } - InvalidAddressError struct{ abciError } - UnknownAddressError struct{ abciError } - InvalidPubKeyError struct{ abciError } - InsufficientCoinsError struct{ abciError } - InvalidCoinsError struct{ abciError } - InvalidGasWantedError struct{ abciError } - OutOfGasError struct{ abciError } - MemoTooLargeError struct{ abciError } - InsufficientFeeError struct{ abciError } - TooManySignaturesError struct{ abciError } - NoSignaturesError struct{ abciError } - GasOverflowError struct{ abciError } + TxDecodeError struct{ abciError } + InvalidSequenceError struct{ abciError } + UnauthorizedError struct{ abciError } + InsufficientFundsError struct{ abciError } + UnknownRequestError struct{ abciError } + InvalidAddressError struct{ abciError } + UnknownAddressError struct{ abciError } + InvalidPubKeyError struct{ abciError } + InsufficientCoinsError struct{ abciError } + InvalidCoinsError struct{ abciError } + InvalidGasWantedError struct{ abciError } + OutOfGasError struct{ abciError } + MemoTooLargeError struct{ abciError } + InsufficientFeeError struct{ abciError } + TooManySignaturesError struct{ abciError } + NoSignaturesError struct{ abciError } + GasOverflowError struct{ abciError } + RestrictedTransferError struct{ abciError } ) -func (e InternalError) Error() string { return "internal error" } -func (e TxDecodeError) Error() string { return "tx decode error" } -func (e InvalidSequenceError) Error() string { return "invalid sequence error" } -func (e UnauthorizedError) Error() string { return "unauthorized error" } -func (e InsufficientFundsError) Error() string { return "insufficient funds error" } -func (e UnknownRequestError) Error() string { return "unknown request error" } -func (e InvalidAddressError) Error() string { return "invalid address error" } -func (e UnknownAddressError) Error() string { return "unknown address error" } -func (e InvalidPubKeyError) Error() string { return "invalid pubkey error" } -func (e InsufficientCoinsError) Error() string { return "insufficient coins error" } -func (e InvalidCoinsError) Error() string { return "invalid coins error" } -func (e InvalidGasWantedError) Error() string { return "invalid gas wanted" } -func (e OutOfGasError) Error() string { return "out of gas error" } -func (e MemoTooLargeError) Error() string { return "memo too large error" } -func (e InsufficientFeeError) Error() string { return "insufficient fee error" } -func (e TooManySignaturesError) Error() string { return "too many signatures error" } -func (e NoSignaturesError) Error() string { return "no signatures error" } -func (e GasOverflowError) Error() string { return "gas overflow error" } +func (e InternalError) Error() string { return "internal error" } +func (e TxDecodeError) Error() string { return "tx decode error" } +func (e InvalidSequenceError) Error() string { return "invalid sequence error" } +func (e UnauthorizedError) Error() string { return "unauthorized error" } +func (e InsufficientFundsError) Error() string { return "insufficient funds error" } +func (e UnknownRequestError) Error() string { return "unknown request error" } +func (e InvalidAddressError) Error() string { return "invalid address error" } +func (e UnknownAddressError) Error() string { return "unknown address error" } +func (e InvalidPubKeyError) Error() string { return "invalid pubkey error" } +func (e InsufficientCoinsError) Error() string { return "insufficient coins error" } +func (e InvalidCoinsError) Error() string { return "invalid coins error" } +func (e InvalidGasWantedError) Error() string { return "invalid gas wanted" } +func (e OutOfGasError) Error() string { return "out of gas error" } +func (e MemoTooLargeError) Error() string { return "memo too large error" } +func (e InsufficientFeeError) Error() string { return "insufficient fee error" } +func (e TooManySignaturesError) Error() string { return "too many signatures error" } +func (e NoSignaturesError) Error() string { return "no signatures error" } +func (e GasOverflowError) Error() string { return "gas overflow error" } +func (e RestrictedTransferError) Error() string { return "restricted token transfer error" } // NOTE also update pkg/std/package.go registrations. diff --git a/tm2/pkg/std/package.go b/tm2/pkg/std/package.go index a1aadc17cb6..471c32f3f5e 100644 --- a/tm2/pkg/std/package.go +++ b/tm2/pkg/std/package.go @@ -36,4 +36,5 @@ var Package = amino.RegisterPackage(amino.NewPackage( TooManySignaturesError{}, "TooManySignaturesError", NoSignaturesError{}, "NoSignaturesError", GasOverflowError{}, "GasOverflowError", + RestrictedTransferError{}, "RestrictedTransferError", ))