diff --git a/examples/gno.land/r/gnops/valopers/filetests/z_2_filetest.gno b/examples/gno.land/r/gnops/valopers/filetests/z_2_filetest.gno index b261ffbdefc..e8c75cee6e9 100644 --- a/examples/gno.land/r/gnops/valopers/filetests/z_2_filetest.gno +++ b/examples/gno.land/r/gnops/valopers/filetests/z_2_filetest.gno @@ -38,6 +38,7 @@ func main() { // Output: // Valoper's details: // ## test-1 +// // test-1's description // // - Operator Address: g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h diff --git a/examples/gno.land/r/gnops/valopers/proposal/proposal.gno b/examples/gno.land/r/gnops/valopers/proposal/proposal.gno index bba566bfb81..aab5fed5162 100644 --- a/examples/gno.land/r/gnops/valopers/proposal/proposal.gno +++ b/examples/gno.land/r/gnops/valopers/proposal/proposal.gno @@ -13,6 +13,7 @@ import ( var ( ErrValidatorMissing = errors.New("the validator is missing") ErrSameValues = errors.New("the valoper has the same voting power and pubkey") + ErrNegativeMinFee = errors.New("min fee must be non-negative") ) // NewValidatorProposalRequest creates a proposal request to the GovDAO @@ -88,5 +89,9 @@ func ProposeNewInstructionsProposalRequest(cur realm, newInstructions string) da // generic sys/params factory so the fee lives in // node:valoper:register_fee. func ProposeNewMinFeeProposalRequest(cur realm, newMinFee int64) dao.ProposalRequest { + if newMinFee < 0 { + panic(ErrNegativeMinFee) + } + return sysparams.NewSysParamUint64PropRequest(cross(cur), "node", "valoper", "register_fee", uint64(newMinFee)) } diff --git a/examples/gno.land/r/gnops/valopers/proposal/proposal_test.gno b/examples/gno.land/r/gnops/valopers/proposal/proposal_test.gno index 39f91f6d751..120fc32afa8 100644 --- a/examples/gno.land/r/gnops/valopers/proposal/proposal_test.gno +++ b/examples/gno.land/r/gnops/valopers/proposal/proposal_test.gno @@ -205,6 +205,14 @@ func TestValopers_ProposeNewMinFee(cur realm, t *testing.T) { urequire.Equal(t, description, proposal.Description()) } +func TestValopers_ProposeNewMinFee_RejectsNegative(cur realm, t *testing.T) { + testing.SetRealm(testing.NewUserRealm(g1user)) + + urequire.AbortsWithMessage(t, cur, ErrNegativeMinFee.Error(), func(cur realm) { + ProposeNewMinFeeProposalRequest(cur, int64(-1)) + }) +} + /* TODO fix this @moul func TestValopers_ProposeNewValidator2(cur realm, t *testing.T) { const ( diff --git a/examples/gno.land/r/gnops/valopers/valopers.gno b/examples/gno.land/r/gnops/valopers/valopers.gno index d052e4d53de..1a7720a8539 100644 --- a/examples/gno.land/r/gnops/valopers/valopers.gno +++ b/examples/gno.land/r/gnops/valopers/valopers.gno @@ -14,6 +14,7 @@ import ( "gno.land/p/nt/avl/v0/pager" "gno.land/p/nt/bptree/v0" "gno.land/p/nt/combinederr/v0" + "gno.land/p/nt/markdown/sanitize/v0" "gno.land/p/nt/ownable/v0" "gno.land/p/nt/ownable/v0/exts/authorizable" "gno.land/p/nt/ufmt/v0" @@ -88,6 +89,9 @@ func (v Valoper) Auth() *authorizable.Authorizable { } func AddToAuthList(cur realm, addr address, member address) { + if err := validateBech32(member); err != nil { + panic(err) + } v := GetByAddr(addr) if err := v.Auth().AddToAuthList(0, cur, member); err != nil { panic(err) @@ -95,6 +99,9 @@ func AddToAuthList(cur realm, addr address, member address) { } func DeleteFromAuthList(cur realm, addr address, member address) { + if err := validateBech32(member); err != nil { + panic(err) + } v := GetByAddr(addr) if err := v.Auth().DeleteFromAuthList(0, cur, member); err != nil { panic(err) @@ -460,10 +467,12 @@ func (v *Valoper) Validate() error { // Render renders a single valoper with their information. func (v Valoper) Render() string { - output := ufmt.Sprintf("## %s\n", v.Moniker) + output := ufmt.Sprintf("## %s", v.Moniker) - if v.Description != "" { - output += ufmt.Sprintf("%s\n\n", v.Description) + if body := sanitize.Block(v.Description); body != "" { + output += body + } else { + output += "\n" } output += ufmt.Sprintf("- Operator Address: %s\n", v.OperatorAddress.String()) diff --git a/examples/gno.land/r/gnops/valopers/valopers_test.gno b/examples/gno.land/r/gnops/valopers/valopers_test.gno index f5738aeade3..cb00ecb1608 100644 --- a/examples/gno.land/r/gnops/valopers/valopers_test.gno +++ b/examples/gno.land/r/gnops/valopers/valopers_test.gno @@ -376,6 +376,67 @@ func TestValopers_UpdateAuthMembers(cur realm, t *testing.T) { uassert.Equal(t, newMoniker, valoper.Moniker) }) }) + + t.Run("invalid member address rejected", func(cur realm, t *testing.T) { + resetState() + + info := validValidatorInfo(t) + testing.SetRealm(testing.NewUserRealm(info.Address)) + testing.SetOriginSend(chain.Coins{minFee}) + + uassert.NotPanics(t, cur, func() { + Register(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey) + }) + + // Caller is the owner, but the member address is invalid: the + // bech32 guard rejects it before reaching the auth list. Empty + // and malformed-but-nonempty (bad checksum) are both rejected, + // so a future weakening to a mere empty-check would fail here. + for _, bad := range []address{address(""), address("g1nonsense")} { + uassert.AbortsWithMessage(t, cur, ErrInvalidAddress.Error(), func() { + AddToAuthList(cross(cur), info.Address, bad) + }) + + uassert.AbortsWithMessage(t, cur, ErrInvalidAddress.Error(), func() { + DeleteFromAuthList(cross(cur), info.Address, bad) + }) + } + }) +} + +func TestValopers_RenderSanitizesDescription(cur realm, t *testing.T) { + resetState() + + info := validValidatorInfo(t) + // Description that tries to inject a top-level heading. Input + // validation accepts it (only length is checked); Render must + // neutralize the structural markdown before it reaches the profile + // page and the govdao proposal that embeds Render. + info.Description = "# FORGEDHEADING\nbody FORGEDBODY" + + testing.SetRealm(testing.NewUserRealm(info.Address)) + testing.SetOriginSend(chain.Coins{minFee}) + uassert.NotPanics(t, cur, func() { + Register(cross(cur), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey) + }) + + out := GetByAddr(info.Address).Render() + + // Prose survives. + uassert.True(t, strings.Contains(out, "FORGEDBODY"), "description body must be preserved") + uassert.True(t, strings.Contains(out, "FORGEDHEADING"), "heading text must be preserved as prose") + // But not as a live ATX heading line. + uassert.False(t, strings.Contains(out, "\n# FORGEDHEADING"), "forged heading must be neutralized") + + // A non-empty description that sanitizes to empty (lone link- + // reference definition) must not glue the heading onto the chrome. + testing.SetRealm(testing.NewUserRealm(info.Address)) + uassert.NotPanics(t, cur, func() { + UpdateDescription(cross(cur), info.Address, "[a]: https://x.example") + }) + out2 := GetByAddr(info.Address).Render() + uassert.True(t, strings.Contains(out2, "## "+info.Moniker+"\n"), "heading line must stay terminated") + uassert.True(t, strings.Contains(out2, "\n- Operator Address:"), "operator-address line must not merge into the heading") } func TestValopers_UpdateMoniker(cur realm, t *testing.T) {