Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

joinprice_test.gno

4.49 Kb · 119 lines
  1package crews
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"testing"
  7
  8	"chain"
  9
 10	"gno.land/p/moul/x/social/crew/v0"
 11	"gno.land/p/nt/uassert/v0"
 12	"gno.land/p/nt/urequire/v0"
 13)
 14
 15// The price a crew page shows a joiner, and the floor Join's envelope guard
 16// enforces, must both be exactly the least amount that mints a whole share.
 17//
 18// Join mints amount * totalShares / treasury, rounded down, so that amount is
 19// treasury / totalShares rounded UP. Rounded down instead, it is one ugnot
 20// short whenever the treasury is not an exact multiple of the share count,
 21// which is the ordinary state after any Fund: the page quotes a price, the
 22// guard accepts it, and the join reverts on "buys no whole share".
 23//
 24// The table is over that rule, not over one input: awkward remainders where
 25// floor and ceiling differ, exact multiples where they must not, and a crew
 26// everybody left, where the opening price applies.
 27func TestTheShownPriceBuysAShareAndOneLessIsRefusedByTheGuard(cur realm, t *testing.T) {
 28	cases := []struct {
 29		name   string
 30		create int64 // what the founder pays in, at the opening price
 31		fund   int64 // then funded with this, which mints nothing
 32		leave  bool  // then the founder ragequits, leaving an empty crew
 33		want   int64 // the least amount that mints one share
 34	}{
 35		{"treasury 3001 over 2 shares", 2000, 1001, false, 1501},
 36		{"treasury 3001 over 3 shares", 3000, 1, false, 1001},
 37		{"ten GNOT and seven ugnot over 10000 shares", tenGNOT, 7, false, 1001},
 38		{"ten GNOT and 999 ugnot over 10000 shares", tenGNOT, 999, false, 1001},
 39		{"exact multiple at the opening price", tenGNOT, 0, false, 1000},
 40		{"exact multiple after doubling", tenGNOT, tenGNOT, false, 2000},
 41		{"a single share", 1000, 0, false, 1000},
 42		{"everybody left", 1000, 0, true, crew.InitialPricePerShare},
 43	}
 44	for _, tc := range cases {
 45		reset()
 46		id := create(cur, alice, tc.create, "crew")
 47		if tc.fund > 0 {
 48			fund(cur, carol, tc.fund, id)
 49		}
 50		if tc.leave {
 51			testing.SetRealm(testing.NewUserRealm(alice))
 52			Ragequit(cross(cur), id)
 53		}
 54
 55		// The oracle, independent of the code under test: the least a with
 56		// a * shares >= treasury, or the opening price when nobody holds one.
 57		_, _, _, shares, treasury := Get(id)
 58		oracle := crew.InitialPricePerShare
 59		if shares > 0 && treasury > 0 {
 60			oracle = treasury / shares
 61			if treasury%shares != 0 {
 62				oracle++
 63			}
 64		}
 65		urequire.Equal(t, tc.want, oracle, tc.name+": the table agrees with the rule")
 66
 67		page := Render("crew/" + strconv.FormatInt(id, 10))
 68		uassert.Equal(t, tc.want, shownPrice(t, page),
 69			tc.name+": the page quotes what buys a share: "+page)
 70
 71		// One ugnot under it is refused by the envelope guard, which names the
 72		// real price, rather than by Join after the guard let it through.
 73		testing.IssueCoins(realmAddr, chain.Coins{{denom, tc.want - 1}})
 74		testing.SetOriginSend(chain.Coins{{denom, tc.want - 1}})
 75		testing.SetRealm(testing.NewUserRealm(bob))
 76		uassert.AbortsContains(t, cur, "costs at least "+strconv.FormatInt(tc.want, 10)+denom,
 77			func() { Join(cross(cur), id) }, tc.name+": the guard refuses one ugnot short")
 78
 79		// Exactly the quoted price buys exactly one share.
 80		got := join(cur, bob, tc.want, id)
 81		uassert.Equal(t, int64(1), got, tc.name+": the quoted price mints one share")
 82	}
 83}
 84
 85// shownPrice reads the price cell of a crew page's summary table, the first
 86// row after the header that starts with "| members", in ugnot. It is keyed on
 87// the row's position, not the column's title, so it reads the same cell
 88// whatever that column is called.
 89func shownPrice(t *testing.T, page string) int64 {
 90	lines := strings.Split(page, "\n")
 91	for i, l := range lines {
 92		if !strings.HasPrefix(l, "| members") {
 93			continue
 94		}
 95		urequire.True(t, i+2 < len(lines), "the summary table has a row: "+page)
 96		cells := strings.Split(lines[i+2], "|")
 97		urequire.Equal(t, 6, len(cells), "four cells: "+lines[i+2])
 98		return parseGNOT(t, strings.TrimSpace(cells[4]))
 99	}
100	t.Fatalf("no summary table on the crew page: %s", page)
101	return 0
102}
103
104// parseGNOT turns num.GNOTf's "1.0015 GNOT" back into ugnot.
105func parseGNOT(t *testing.T, s string) int64 {
106	urequire.True(t, strings.HasSuffix(s, " GNOT"), "a GNOT amount: "+s)
107	s = strings.TrimSuffix(s, " GNOT")
108	whole, frac := s, ""
109	if i := strings.Index(s, "."); i >= 0 {
110		whole, frac = s[:i], s[i+1:]
111	}
112	urequire.True(t, len(frac) <= 6, "at most six decimals: "+s)
113	frac += strings.Repeat("0", 6-len(frac))
114	w, err := strconv.ParseInt(whole, 10, 64)
115	urequire.NoError(t, err, s)
116	f, err := strconv.ParseInt(frac, 10, 64)
117	urequire.NoError(t, err, s)
118	return w*1000000 + f
119}