1package curated23import (4 "strings"5 "testing"67 "gno.land/p/nt/testutils/v0"8 "gno.land/p/nt/uassert/v0"9 "gno.land/p/nt/urequire/v0"10)1112const (13 deposit = int64(1000)14 window = int64(100)15)1617var (18 alice = testutils.TestAddress("alice")19 bob = testutils.TestAddress("bob")20 carol = testutils.TestAddress("carol")21 dave = testutils.TestAddress("dave")22)2324// list returns a registry holding one entry owned by alice, listed at height25// 100, which is the starting point of most cases below.26func list(t *testing.T) *Registry {27 t.Helper()28 r := New(deposit, window)29 urequire.NoError(t, r.Apply("gnoswap", "https://gnoswap.io", "an AMM", alice, deposit, 100))30 return r31}3233func TestValidKey(t *testing.T) {34 tests := []struct {35 key string36 want bool37 }{38 {"gnoswap", true},39 {"a", true},40 {"0", true},41 {"gno-swap_v2.1", true},42 {strings.Repeat("x", MaxKeyLen), true},43 {"", false},44 {strings.Repeat("x", MaxKeyLen+1), false},45 {"GnoSwap", false},46 {"gno swap", false},47 {"gno/swap", false},48 {"gno:swap", false},49 {".", false},50 {"..", false},51 {"-leading", false},52 {"_leading", false},53 {"émoji", false},54 }55 for _, tt := range tests {56 uassert.Equal(t, tt.want, ValidKey(tt.key), tt.key)57 }58}5960func TestValidURL(t *testing.T) {61 tests := []struct {62 name string63 url string64 want bool65 }{66 {"https", "https://gnoswap.io/pools", true},67 {"an on-chain path, which has no scheme to carry", "/r/moul/home", true},68 {"query and fragment", "https://x.io/a?b=c#d", true},69 {"a pipe is escaped at render time, not refused", "https://x.io/a|b", true},70 {"empty", "", false},71 {"too long", "https://x.io/" + strings.Repeat("x", MaxURLLen), false},72 {"a space", "https://x.io/a b", false},73 {"a newline", "https://x.io/a\nb", false},74 {"a control character", "https://x.io/a\x01b", false},75 }76 for _, tt := range tests {77 uassert.Equal(t, tt.want, ValidURL(tt.url), tt.name)78 }79}8081func TestValidDescription(t *testing.T) {82 tests := []struct {83 name string84 desc string85 want bool86 }{87 {"plain", "an AMM on gno.land", true},88 {"markdown is allowed and escaped later", "[x](y) | z", true},89 {"empty", "", false},90 {"blank", " ", false},91 {"too long", strings.Repeat("x", MaxDescLen+1), false},92 {"a newline would break its own table cell", "a\nb", false},93 {"a tab, same reason", "a\tb", false},94 }95 for _, tt := range tests {96 uassert.Equal(t, tt.want, ValidDescription(tt.desc), tt.name)97 }98}99100func TestApplyRefusesWhatItCannotStore(t *testing.T) {101 tests := []struct {102 name string103 key string104 url string105 desc string106 paid int64107 want error108 }{109 {"bad key", "Gnoswap", "https://x.io", "an AMM", deposit, ErrBadKey},110 {"bad url", "gnoswap", "", "an AMM", deposit, ErrBadURL},111 {"bad description", "gnoswap", "https://x.io", " ", deposit, ErrBadDescription},112 {"underpaid", "gnoswap", "https://x.io", "an AMM", deposit - 1, ErrWrongDeposit},113 {"overpaid", "gnoswap", "https://x.io", "an AMM", deposit + 1, ErrWrongDeposit},114 {"nothing attached", "gnoswap", "https://x.io", "an AMM", 0, ErrWrongDeposit},115 }116 for _, tt := range tests {117 r := New(deposit, window)118 err := r.Apply(tt.key, tt.url, tt.desc, alice, tt.paid, 100)119 uassert.ErrorIs(t, err, tt.want, tt.name)120 uassert.Equal(t, 0, r.Count(), tt.name+": nothing was listed")121 uassert.Equal(t, int64(0), r.Locked(), tt.name+": nothing was locked")122 }123}124125func TestApplyListsImmediately(t *testing.T) {126 r := list(t)127128 e, ok := r.Get("gnoswap")129 urequire.True(t, ok, "the entry exists")130 uassert.Equal(t, "https://gnoswap.io", e.URL)131 uassert.Equal(t, "an AMM", e.Description)132 uassert.Equal(t, alice.String(), e.Owner.String())133 uassert.Equal(t, deposit, e.Deposit)134 uassert.Equal(t, int64(100), e.At)135 uassert.Equal(t, "listed", e.State.String())136137 uassert.True(t, r.IsListed("gnoswap"))138 uassert.False(t, r.IsListed("nothing"))139 uassert.Equal(t, 1, r.Count())140 uassert.Equal(t, 1, r.Records())141 uassert.Equal(t, deposit, r.Locked())142 uassert.Equal(t, int64(0), r.Owed())143}144145// A key is unique while it is on the list, and free again once it is off it: a146// challenge removes an entry, it does not burn the name.147func TestAKeyIsTakenUntilItIsRemoved(t *testing.T) {148 r := list(t)149150 uassert.ErrorIs(t, r.Apply("gnoswap", "https://other.io", "mine now", bob, deposit, 101), ErrTaken)151 urequire.NoError(t, r.Unlist("gnoswap", alice))152 uassert.False(t, r.IsListed("gnoswap"))153154 urequire.NoError(t, r.Apply("gnoswap", "https://other.io", "mine now", bob, deposit, 102))155 e, _ := r.Get("gnoswap")156 uassert.Equal(t, bob.String(), e.Owner.String())157 uassert.Equal(t, int64(102), e.At, "a re-application is a fresh entry")158 uassert.Equal(t, 1, r.Count())159 uassert.Equal(t, 1, r.Records(), "and it reuses its place in the listing order")160}161162func TestListedIsOldestFirstAndSkipsTheRemoved(t *testing.T) {163 r := New(deposit, window)164 urequire.NoError(t, r.Apply("one", "https://1.io", "first", alice, deposit, 100))165 urequire.NoError(t, r.Apply("two", "https://2.io", "second", alice, deposit, 101))166 urequire.NoError(t, r.Apply("three", "https://3.io", "third", alice, deposit, 102))167168 got := r.Listed()169 urequire.Equal(t, 3, len(got))170 uassert.Equal(t, "one", got[0].Key)171 uassert.Equal(t, "two", got[1].Key)172 uassert.Equal(t, "three", got[2].Key)173174 urequire.NoError(t, r.Unlist("two", alice))175 got = r.Listed()176 urequire.Equal(t, 2, len(got))177 uassert.Equal(t, "one", got[0].Key)178 uassert.Equal(t, "three", got[1].Key)179 uassert.Equal(t, 0, len(r.Challenged()))180}181182func TestChallengeRefusals(t *testing.T) {183 tests := []struct {184 name string185 key string186 who address187 bond int64188 want error189 }{190 {"no such entry", "nothing", bob, deposit, ErrNoEntry},191 {"the bond must match", "gnoswap", bob, deposit - 1, ErrWrongBond},192 {"and may not exceed it either", "gnoswap", bob, deposit + 1, ErrWrongBond},193 {"an owner cannot shield their own entry", "gnoswap", alice, deposit, ErrSelfChallenge},194 }195 for _, tt := range tests {196 r := list(t)197 uassert.ErrorIs(t, r.Challenge(tt.key, tt.who, tt.bond, 200), tt.want, tt.name)198 uassert.Equal(t, deposit, r.Locked(), tt.name+": no bond was taken")199 _, open := r.ChallengeOf("gnoswap")200 uassert.False(t, open, tt.name+": no challenge was opened")201 }202}203204func TestChallengeOpensOnceAndSetsTheDeadline(t *testing.T) {205 r := list(t)206207 bond, ok := r.BondFor("gnoswap")208 urequire.True(t, ok, "it is challengeable")209 uassert.Equal(t, deposit, bond, "the bond matches that entry's own deposit")210211 urequire.NoError(t, r.Challenge("gnoswap", bob, bond, 200))212 c, open := r.ChallengeOf("gnoswap")213 urequire.True(t, open, "the challenge is readable")214 uassert.Equal(t, bob.String(), c.Challenger.String())215 uassert.Equal(t, int64(200+window), c.Deadline)216 uassert.True(t, c.Open(200+window-1))217 uassert.False(t, c.Open(200+window), "the deadline is exclusive")218 uassert.Equal(t, "challenged", mustGet(t, r, "gnoswap").State.String())219 uassert.True(t, r.IsListed("gnoswap"), "a challenge is an objection, not a verdict")220 uassert.Equal(t, 2*deposit, r.Locked(), "the deposit and the bond")221222 // A second challenge would be a second bond on one entry.223 uassert.ErrorIs(t, r.Challenge("gnoswap", carol, deposit, 201), ErrChallenged)224 _, ok = r.BondFor("gnoswap")225 uassert.False(t, ok, "and BondFor says so before any money is read")226}227228func TestVoteIsOneAddressOneVoteWhileTheChallengeIsOpen(t *testing.T) {229 r := list(t)230 uassert.ErrorIs(t, r.Vote("gnoswap", carol, true, 200), ErrNoChallenge)231 uassert.ErrorIs(t, r.Vote("nothing", carol, true, 200), ErrNoEntry)232233 urequire.NoError(t, r.Challenge("gnoswap", bob, deposit, 200))234 urequire.NoError(t, r.Vote("gnoswap", carol, true, 210))235 uassert.ErrorIs(t, r.Vote("gnoswap", carol, false, 211), ErrAlreadyVoted)236 uassert.ErrorIs(t, r.Vote("gnoswap", carol, true, 212), ErrAlreadyVoted)237 urequire.NoError(t, r.Vote("gnoswap", dave, false, 213))238239 c, _ := r.ChallengeOf("gnoswap")240 uassert.Equal(t, int64(1), c.Keep)241 uassert.Equal(t, int64(1), c.Remove)242 uassert.Equal(t, 2, c.Voters())243 uassert.True(t, c.HasVoted(carol))244 uassert.False(t, c.HasVoted(alice))245246 // Past the deadline the poll is shut, and so is the deadline itself.247 uassert.ErrorIs(t, r.Vote("gnoswap", alice, true, 200+window), ErrVotingClosed)248 uassert.Equal(t, 2, c.Voters())249}250251// The outcome table: who wins, and what the loser pays. A tie keeps the entry.252func TestResolvePaysTheWinnerAndTiesKeep(t *testing.T) {253 tests := []struct {254 name string255 keep []address256 remove []address257 wantKept bool258 wantWinner address259 wantAmount int64260 }{261 {"nobody voted, the incumbent keeps it", nil, nil, true, alice, deposit},262 {"a tie keeps it", []address{carol}, []address{dave}, true, alice, deposit},263 {"a majority to keep", []address{carol, dave}, nil, true, alice, deposit},264 {"a majority to remove", nil, []address{carol, dave}, false, bob, 2 * deposit},265 }266 for _, tt := range tests {267 r := list(t)268 urequire.NoError(t, r.Challenge("gnoswap", bob, deposit, 200))269 for _, who := range tt.keep {270 urequire.NoError(t, r.Vote("gnoswap", who, true, 210))271 }272 for _, who := range tt.remove {273 urequire.NoError(t, r.Vote("gnoswap", who, false, 210))274 }275276 out, err := r.Resolve("gnoswap", 200+window)277 urequire.NoError(t, err, tt.name)278 uassert.Equal(t, tt.wantKept, out.Kept, tt.name)279 uassert.Equal(t, tt.wantWinner.String(), out.Winner.String(), tt.name)280 uassert.Equal(t, tt.wantAmount, out.Amount, tt.name)281 uassert.Equal(t, tt.wantAmount, r.CreditOf(tt.wantWinner), tt.name+": credited, not sent")282 uassert.Equal(t, tt.wantKept, r.IsListed("gnoswap"), tt.name)283284 // Whatever happened, every ugnot is either still backing the entry or285 // assigned to somebody.286 uassert.Equal(t, 2*deposit, r.Locked()+r.Owed(), tt.name+": the books balance")287288 // The challenge is gone either way, so nothing resolves twice.289 _, open := r.ChallengeOf("gnoswap")290 uassert.False(t, open, tt.name)291 _, err = r.Resolve("gnoswap", 200+window)292 uassert.ErrorIs(t, err, ErrNoChallenge, tt.name)293 }294}295296func TestResolveRefusesBeforeTheDeadline(t *testing.T) {297 r := list(t)298 _, err := r.Resolve("gnoswap", 200)299 uassert.ErrorIs(t, err, ErrNoChallenge, "an unchallenged entry has nothing to resolve")300 _, err = r.Resolve("nothing", 200)301 uassert.ErrorIs(t, err, ErrNoEntry)302303 urequire.NoError(t, r.Challenge("gnoswap", bob, deposit, 200))304 _, err = r.Resolve("gnoswap", 200+window-1)305 uassert.ErrorIs(t, err, ErrTooEarly)306 uassert.Equal(t, int64(0), r.Owed(), "nothing was paid out early")307 uassert.Equal(t, "challenged", mustGet(t, r, "gnoswap").State.String())308}309310// Losing a challenge frees the key, and the deposit that backed it has already311// gone to the challenger, so re-listing it costs a fresh deposit.312func TestAnEntryThatLosesIsRemovedAndItsDepositIsGone(t *testing.T) {313 r := list(t)314 urequire.NoError(t, r.Challenge("gnoswap", bob, deposit, 200))315 urequire.NoError(t, r.Vote("gnoswap", carol, false, 210))316 _, err := r.Resolve("gnoswap", 200+window)317 urequire.NoError(t, err)318319 uassert.Equal(t, "removed", mustGet(t, r, "gnoswap").State.String())320 uassert.Equal(t, 0, r.Count())321 uassert.Equal(t, int64(0), r.Locked())322 uassert.Equal(t, 2*deposit, r.Owed())323 uassert.Equal(t, int64(0), r.CreditOf(alice), "the owner lost the deposit")324 uassert.ErrorIs(t, r.Unlist("gnoswap", alice), ErrNoEntry, "there is nothing left to take down")325}326327func TestUnlistIsTheOwnersAndNotWhileChallenged(t *testing.T) {328 r := list(t)329 uassert.ErrorIs(t, r.Unlist("nothing", alice), ErrNoEntry)330 uassert.ErrorIs(t, r.Unlist("gnoswap", bob), ErrNotOwner)331332 urequire.NoError(t, r.Challenge("gnoswap", bob, deposit, 200))333 uassert.ErrorIs(t, r.Unlist("gnoswap", alice), ErrChallenged,334 "an owner who could walk away mid-challenge would be risking nothing")335 uassert.Equal(t, int64(0), r.CreditOf(alice))336337 // Once the challenge is behind it, the entry is the owner's to take down.338 urequire.NoError(t, r.Vote("gnoswap", carol, true, 210))339 _, err := r.Resolve("gnoswap", 200+window)340 urequire.NoError(t, err)341 urequire.NoError(t, r.Unlist("gnoswap", alice))342 uassert.Equal(t, 2*deposit, r.CreditOf(alice), "the bond she won, plus her own deposit back")343 uassert.Equal(t, int64(0), r.Locked())344}345346func TestWithdrawZeroesTheCreditBeforeItIsPaid(t *testing.T) {347 r := list(t)348 urequire.NoError(t, r.Unlist("gnoswap", alice))349 uassert.Equal(t, deposit, r.Owed())350351 _, err := r.Withdraw(bob)352 uassert.ErrorIs(t, err, ErrNothingOwed, "bob is owed nothing")353354 got, err := r.Withdraw(alice)355 urequire.NoError(t, err)356 uassert.Equal(t, deposit, got)357 uassert.Equal(t, int64(0), r.CreditOf(alice))358 uassert.Equal(t, int64(0), r.Owed())359360 // A second call, which is what a reentrant one would be, finds nothing.361 _, err = r.Withdraw(alice)362 uassert.ErrorIs(t, err, ErrNothingOwed)363}364365func TestCreditsAccumulateAcrossEntries(t *testing.T) {366 r := list(t)367 urequire.NoError(t, r.Apply("gnoscan", "https://gnoscan.io", "an explorer", alice, deposit, 101))368 urequire.NoError(t, r.Unlist("gnoswap", alice))369 urequire.NoError(t, r.Unlist("gnoscan", alice))370371 uassert.Equal(t, 2*deposit, r.CreditOf(alice), "crediting twice owes the sum")372 got, err := r.Withdraw(alice)373 urequire.NoError(t, err)374 uassert.Equal(t, 2*deposit, got, "and one withdrawal collects both")375}376377// Everything the registry ever took in is either locked behind something live378// or owed to somebody, at every step of a full round.379func TestTheBooksBalanceAtEveryStep(t *testing.T) {380 r := New(deposit, window)381 paidIn := int64(0)382383 check := func(step string) {384 t.Helper()385 uassert.Equal(t, paidIn, r.Locked()+r.Owed(), step)386 }387 check("empty")388389 urequire.NoError(t, r.Apply("one", "https://1.io", "first", alice, deposit, 100))390 paidIn += deposit391 check("after a listing")392393 urequire.NoError(t, r.Challenge("one", bob, deposit, 200))394 paidIn += deposit395 check("after a challenge")396397 urequire.NoError(t, r.Vote("one", carol, false, 210))398 check("after a vote")399400 _, err := r.Resolve("one", 200+window)401 urequire.NoError(t, err)402 check("after a resolution")403404 _, err = r.Withdraw(bob)405 urequire.NoError(t, err)406 paidIn -= 2 * deposit407 check("after the winner collected")408 uassert.Equal(t, int64(0), r.Locked()+r.Owed(), "and the realm owes nothing")409}410411func mustGet(t *testing.T, r *Registry, key string) *Entry {412 t.Helper()413 e, ok := r.Get(key)414 urequire.True(t, ok, "entry "+key+" exists")415 return e416}417Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.