Skip to content

Commit b574b51

Browse files
feat(zkgm): implement token decimal metadata via TokenInitializer (#131)
* feat(zkgm): implement token decimal metadata via TokenInitializer * fix(zkgm): align decimals type to uint8 across voucher interface * test(zkgm): add sanitizeGRC20Symbol and initializer fallback tests * feat(zkgm): dispatch token initializer decoding by implementation type --------- Co-authored-by: Lee ByeongJun <lbj199874@gmail.com>
1 parent 553ae98 commit b574b51

12 files changed

Lines changed: 250 additions & 28 deletions

File tree

gno.land/p/core/ibc/zkgm/abi.gno

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ var TokenMetadataSchema = abi.Schema{Fields: []abi.Field{
7878
{Type: abi.TypeBytes},
7979
}}
8080

81+
var TokenInitializerSchema = abi.Schema{Fields: []abi.Field{
82+
{Type: abi.TypeString},
83+
{Type: abi.TypeString},
84+
{Type: abi.TypeUint8},
85+
}}
86+
8187
var SolverMetadataSchema = abi.Schema{Fields: []abi.Field{
8288
{Type: abi.TypeBytes},
8389
{Type: abi.TypeBytes},
@@ -244,6 +250,37 @@ func DecodeTokenOrderV2(data []byte) (TokenOrderV2, error) {
244250
}, nil
245251
}
246252

253+
func EncodeTokenInitializer(i TokenInitializer) ([]byte, error) {
254+
return abi.Encode(TokenInitializerSchema, []any{i.Name, i.Symbol, i.Decimals})
255+
}
256+
257+
func DecodeTokenInitializer(data []byte) (TokenInitializer, error) {
258+
out, err := abi.Decode(TokenInitializerSchema, data)
259+
if err != nil {
260+
return TokenInitializer{}, err
261+
}
262+
return TokenInitializer{
263+
Name: out[0].(string),
264+
Symbol: out[1].(string),
265+
Decimals: out[2].(uint8),
266+
}, nil
267+
}
268+
269+
// DecodeTokenInitializerFromMetadata extracts display metadata (name, symbol,
270+
// decimals) from a TokenMetadata by dispatching on the Implementation field.
271+
// New implementation types should be added as explicit cases; unknown formats
272+
// return a zero-value TokenInitializer so voucher creation can proceed with
273+
// safe defaults derived from ibcDenom.
274+
func DecodeTokenInitializerFromMetadata(meta TokenMetadata) TokenInitializer {
275+
switch string(meta.Implementation) {
276+
case "grc20":
277+
if ti, err := DecodeTokenInitializer(meta.Initializer); err == nil {
278+
return ti
279+
}
280+
}
281+
return TokenInitializer{}
282+
}
283+
247284
func EncodeTokenMetadata(m TokenMetadata) ([]byte, error) {
248285
return abi.Encode(TokenMetadataSchema, []any{
249286
m.Implementation,

gno.land/p/core/ibc/zkgm/types.gno

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ type TokenMetadata struct {
8585
Initializer []byte
8686
}
8787

88+
// TokenInitializer holds token display metadata; decimals are metadata only, never used for scaling.
89+
type TokenInitializer struct {
90+
Name string
91+
Symbol string
92+
Decimals uint8
93+
}
94+
8895
// SolverMetadata carries market maker solver data.
8996
//
9097
// Mirrors `SolverMetadata` at com.rs:98.

gno.land/r/core/ibc/v1/apps/zkgm/testing/e2e/helpers.gno

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,14 @@ func MustPacketData(cur realm, instr z.Instruction) []byte {
200200
return packetBytes
201201
}
202202

203-
func MustTokenMetadata(cur realm, implementation string, initializer string) []byte {
203+
func MustTokenMetadata(cur realm, implementation string, ti z.TokenInitializer) []byte {
204+
initBytes, err := z.EncodeTokenInitializer(ti)
205+
if err != nil {
206+
panic(err)
207+
}
204208
meta, err := z.EncodeTokenMetadata(z.TokenMetadata{
205209
Implementation: []byte(implementation),
206-
Initializer: []byte(initializer),
210+
Initializer: initBytes,
207211
})
208212
if err != nil {
209213
panic(err)

gno.land/r/core/ibc/v1/apps/zkgm/testing/e2e/scenarios_batchsend/z21_v1_create_client_handshake_send_filetest.gno

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func main(cur realm) {
2121
QuoteToken: []byte("quote"),
2222
QuoteAmount: u256.Zero(),
2323
Kind: z.TOKEN_ORDER_KIND_INITIALIZE,
24-
Metadata: e2e.MustTokenMetadata(cross(cur), "grc20", "init"),
24+
Metadata: e2e.MustTokenMetadata(cross(cur), "grc20", z.TokenInitializer{Name: "Gno Token", Symbol: "GNOT", Decimals: 6}),
2525
}
2626
packet := core.NewPacket(cross(cur), pair.Source, pair.Destination, e2e.MustPacketData(cross(cur), e2e.MustTokenOrderInstruction(cross(cur), order)), 0, core.Timestamp(3600))
2727
e2e.BatchSend(cross(cur), packet)

gno.land/r/core/ibc/v1/apps/zkgm/testing/e2e/scenarios_batchsend/z23_v1_token_order_roundtrip_filetest.gno

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func main(cur realm) {
1616

1717
sender := "g17xpfvakm2amg962yls6f84z3kell8c5lserqta"
1818
receiver := "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
19-
metaBytes := e2e.MustTokenMetadata(cross(cur), "grc20", "roundtrip")
19+
metaBytes := e2e.MustTokenMetadata(cross(cur), "grc20", z.TokenInitializer{Name: "Gno Token", Symbol: "GNOT", Decimals: 6})
2020
quoteToken := impl.PredictWrappedTokenV2(u256.Zero(), uint32(pair.Destination), []byte("ugnot"), impl.MetadataImage(mustDecodeTokenMetadata(metaBytes)))
2121
order := z.TokenOrderV2{
2222
Sender: []byte(sender),

gno.land/r/core/ibc/v1/apps/zkgm/v0/impl/batch_test.gno

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestExecuteBatchTokenOrderAndCallBothSucceed(cur realm, t *testing.T) {
1919
relayer := testAddress("batch-hook-relayer")
2020
metaBytes := mustEncodeTokenMetadata(z.TokenMetadata{
2121
Implementation: []byte("grc20"),
22-
Initializer: []byte("init"),
22+
Initializer: mustEncodeTokenInitializer(z.TokenInitializer{Name: "Gno Token", Symbol: "GNOT", Decimals: 6}),
2323
})
2424
image := MetadataImage(mustDecodeTokenMetadata(metaBytes))
2525
path := u256.NewUint(9)
@@ -153,7 +153,7 @@ func TestVerifyBatchMixedNativeAndVoucher(cur realm, t *testing.T) {
153153
resetTokenOrderTest(cur)
154154
sender := testAddress("batch-mixed-sender")
155155
voucherDenom := "ibc/batch-mixed"
156-
uassert.NoError(t, mintVoucher(cross(cur), voucherDenom, voucherDenom, [32]byte{}, sender, u256.NewUint(20)))
156+
uassert.NoError(t, mintVoucher(cross(cur), voucherDenom, voucherDenom, [32]byte{}, "", "", 0, sender, u256.NewUint(20)))
157157
instr := batchMustBatchInstruction(z.Batch{Instructions: []z.Instruction{
158158
batchMustOrderInstruction(z.TokenOrderV2{
159159
Sender: []byte(sender),

gno.land/r/core/ibc/v1/apps/zkgm/v0/impl/dispatch_test.gno

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func TestRecvTokenOrderEscrowFeeVoucherMintFailureAborts(cur realm, t *testing.T
9494
// Saturate supply to MaxInt64-1. After receiver mint (+1) total = MaxInt64.
9595
// Fee mint (+1) then overflows int64 → GRC20 returns error → execFatal.
9696
fillAmt := u256.NewUint(uint64(math.MaxInt64) - 1)
97-
uassert.NoError(t, mintVoucher(cross(cur), ibcDenom, string(baseToken), [32]byte{}, testAddress("sat"), fillAmt))
97+
uassert.NoError(t, mintVoucher(cross(cur), ibcDenom, string(baseToken), [32]byte{}, "", "", 0, testAddress("sat"), fillAmt))
9898

9999
order := z.TokenOrderV2{
100100
Kind: z.TOKEN_ORDER_KIND_ESCROW,

gno.land/r/core/ibc/v1/apps/zkgm/v0/impl/forward_test.gno

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestVerifyForwardPassesAllowedChildren(cur realm, t *testing.T) {
2929

3030
sender := testAddress("forward-sender")
3131
baseToken := "ibc/v1-forward"
32-
uassert.NoError(t, mintVoucher(cross(cur), baseToken, baseToken, [32]byte{}, sender, u256.NewUint(30)))
32+
uassert.NoError(t, mintVoucher(cross(cur), baseToken, baseToken, [32]byte{}, "", "", 0, sender, u256.NewUint(30)))
3333
order := z.TokenOrderV2{
3434
Sender: []byte(sender),
3535
BaseToken: []byte(baseToken),

gno.land/r/core/ibc/v1/apps/zkgm/v0/impl/token_order.gno

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ func (v *ZkgmV1) executeTokenOrderV2(cur realm, packet core.Packet, relayer addr
9898
return core.NewRecvPacketResult(cross(cur), core.PacketStatusUnknown, nil), err
9999
}
100100

101+
ti := z.DecodeTokenInitializerFromMetadata(meta)
102+
101103
image := MetadataImage(meta)
102104
destChan, err := tokenOrderDestinationChannel(packet.DestinationChannelId)
103105
if err != nil {
@@ -119,12 +121,12 @@ func (v *ZkgmV1) executeTokenOrderV2(cur realm, packet core.Packet, relayer addr
119121
return core.NewRecvPacketResult(cross(cur), core.PacketStatusUnknown, nil), err
120122
}
121123
if !order.QuoteAmount.IsZero() {
122-
if err := mintVoucher(cross(cur), ibcDenom, string(order.BaseToken), image, string(order.Receiver), order.QuoteAmount); err != nil {
124+
if err := mintVoucher(cross(cur), ibcDenom, string(order.BaseToken), image, ti.Name, ti.Symbol, ti.Decimals, string(order.Receiver), order.QuoteAmount); err != nil {
123125
return core.NewRecvPacketResult(cross(cur), core.PacketStatusUnknown, nil), err
124126
}
125127
}
126128
if !fee.IsZero() {
127-
if err := mintVoucher(cross(cur), ibcDenom, string(order.BaseToken), image, string(relayer), fee); err != nil {
129+
if err := mintVoucher(cross(cur), ibcDenom, string(order.BaseToken), image, ti.Name, ti.Symbol, ti.Decimals, string(relayer), fee); err != nil {
128130
panic(execFatal{err.Error()})
129131
}
130132
}
@@ -162,12 +164,12 @@ func (v *ZkgmV1) executeTokenOrderV2(cur realm, packet core.Packet, relayer addr
162164
}
163165
}
164166
if !order.QuoteAmount.IsZero() {
165-
if err := mintVoucher(cross(cur), quoteDenom, string(order.BaseToken), image, string(order.Receiver), order.QuoteAmount); err != nil {
167+
if err := mintVoucher(cross(cur), quoteDenom, string(order.BaseToken), image, "", "", 0, string(order.Receiver), order.QuoteAmount); err != nil {
166168
return core.NewRecvPacketResult(cross(cur), core.PacketStatusUnknown, nil), err
167169
}
168170
}
169171
if !fee.IsZero() {
170-
if err := mintVoucher(cross(cur), quoteDenom, string(order.BaseToken), image, string(relayer), fee); err != nil {
172+
if err := mintVoucher(cross(cur), quoteDenom, string(order.BaseToken), image, "", "", 0, string(relayer), fee); err != nil {
171173
panic(execFatal{err.Error()})
172174
}
173175
}
@@ -321,7 +323,7 @@ func settleEscrowedV2(cur realm, sourceChannelId core.ChannelId, path *u256.Uint
321323
func refundIBCVoucher(cur realm, baseToken []byte, recipient string, amount *u256.Uint) error {
322324
baseDenom := string(baseToken)
323325
image, _ := zkgm.GetMetadataImageOf(baseDenom)
324-
return mintVoucher(cross(cur), baseDenom, baseDenom, image, recipient, amount)
326+
return mintVoucher(cross(cur), baseDenom, baseDenom, image, "", "", 0, recipient, amount)
325327
}
326328

327329
func sendNative(cur realm, denom string, receiver string, amount *u256.Uint) error {

gno.land/r/core/ibc/v1/apps/zkgm/v0/impl/token_order_test.gno

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func TestTokenOrderVerifyEscrowBurnsVoucherAndIncreasesChannelBalance(cur realm,
2121

2222
sender := testAddress("v1-sender")
2323
baseToken := "ibc/v1-send"
24-
uassert.NoError(t, mintVoucher(cross(cur), baseToken, baseToken, [32]byte{}, sender, u256.NewUint(50)))
24+
uassert.NoError(t, mintVoucher(cross(cur), baseToken, baseToken, [32]byte{}, "", "", 0, sender, u256.NewUint(50)))
2525
order := z.TokenOrderV2{
2626
Sender: []byte(sender),
2727
BaseToken: []byte(baseToken),
@@ -48,7 +48,7 @@ func TestTokenOrderAckMarketMakerEscrowWithVoucherRewardsMarketMaker(cur realm,
4848
baseToken := "ibc/v1-mm-voucher"
4949
channelId := core.ChannelId(8)
5050
amount := u256.NewUint(13)
51-
uassert.NoError(t, mintVoucher(cross(cur), baseToken, baseToken, [32]byte{}, sender, u256.NewUint(50)))
51+
uassert.NoError(t, mintVoucher(cross(cur), baseToken, baseToken, [32]byte{}, "", "", 0, sender, u256.NewUint(50)))
5252

5353
order := z.TokenOrderV2{
5454
Sender: []byte(sender),
@@ -135,7 +135,7 @@ func TestTokenOrderExecuteInitializeMintsVoucherAndSuccessAck(cur realm, t *test
135135

136136
metaBytes := mustEncodeTokenMetadata(z.TokenMetadata{
137137
Implementation: []byte("grc20"),
138-
Initializer: []byte("init"),
138+
Initializer: mustEncodeTokenInitializer(z.TokenInitializer{Name: "Gno Token", Symbol: "GNOT", Decimals: 6}),
139139
})
140140
image := MetadataImage(mustDecodeTokenMetadata(metaBytes))
141141
ibcDenom := PredictWrappedTokenV2(u256.NewUint(7), 5, []byte("ugnot"), image)
@@ -166,6 +166,77 @@ func TestTokenOrderExecuteInitializeMintsVoucherAndSuccessAck(cur realm, t *test
166166
origin, ok := zkgm.GetTokenOrigin(ibcDenom)
167167
uassert.True(t, ok)
168168
uassert.Equal(t, "21474836487", origin.ToString())
169+
170+
v := getVoucher(ibcDenom)
171+
uassert.True(t, v != nil)
172+
uassert.Equal(t, "Gno Token", v.token.GetName())
173+
uassert.Equal(t, "GNOT", v.token.GetSymbol())
174+
uassert.Equal(t, 6, v.token.GetDecimals())
175+
}
176+
177+
func TestTokenOrderExecuteInitializeMalformedInitializerUsesDefaults(cur realm, t *testing.T) {
178+
resetTokenOrderTest(cur)
179+
180+
// Initializer is raw bytes that cannot be decoded as TokenInitializer.
181+
// The protocol must fall back to ibcDenom-derived name/symbol and 0 decimals.
182+
metaBytes := mustEncodeTokenMetadata(z.TokenMetadata{
183+
Implementation: []byte("grc20"),
184+
Initializer: []byte("not-valid-abi"),
185+
})
186+
image := MetadataImage(mustDecodeTokenMetadata(metaBytes))
187+
ibcDenom := PredictWrappedTokenV2(u256.NewUint(3), 4, []byte("ugnot"), image)
188+
order := z.TokenOrderV2{
189+
Sender: []byte("sender"),
190+
Receiver: []byte(testAddress("fallback-receiver")),
191+
BaseToken: []byte("ugnot"),
192+
BaseAmount: u256.NewUint(50),
193+
QuoteToken: []byte(ibcDenom),
194+
QuoteAmount: u256.NewUint(50),
195+
Kind: z.TOKEN_ORDER_KIND_INITIALIZE,
196+
Metadata: metaBytes,
197+
}
198+
packet := core.NewPacket(cross(cur), core.ChannelId(1), core.ChannelId(4), nil, 0, 0)
199+
200+
res, err := (&ZkgmV1{}).executeTokenOrderV2(cur, packet, address(testAddress("fallback-relayer")), u256.NewUint(3), order, false)
201+
202+
uassert.NoError(t, err)
203+
uassert.True(t, res.Status == core.PacketStatusSuccess)
204+
205+
v := getVoucher(ibcDenom)
206+
uassert.True(t, v != nil)
207+
uassert.Equal(t, 0, v.token.GetDecimals())
208+
}
209+
210+
func TestTokenOrderExecuteInitializeEmptyInitializerUsesDefaults(cur realm, t *testing.T) {
211+
resetTokenOrderTest(cur)
212+
213+
// Empty Initializer: decimals must default to 0, symbol derived from ibcDenom.
214+
metaBytes := mustEncodeTokenMetadata(z.TokenMetadata{
215+
Implementation: []byte("grc20"),
216+
Initializer: []byte{},
217+
})
218+
image := MetadataImage(mustDecodeTokenMetadata(metaBytes))
219+
ibcDenom := PredictWrappedTokenV2(u256.NewUint(5), 6, []byte("ugnot"), image)
220+
order := z.TokenOrderV2{
221+
Sender: []byte("sender"),
222+
Receiver: []byte(testAddress("empty-init-receiver")),
223+
BaseToken: []byte("ugnot"),
224+
BaseAmount: u256.NewUint(20),
225+
QuoteToken: []byte(ibcDenom),
226+
QuoteAmount: u256.NewUint(20),
227+
Kind: z.TOKEN_ORDER_KIND_INITIALIZE,
228+
Metadata: metaBytes,
229+
}
230+
packet := core.NewPacket(cross(cur), core.ChannelId(1), core.ChannelId(6), nil, 0, 0)
231+
232+
res, err := (&ZkgmV1{}).executeTokenOrderV2(cur, packet, address(testAddress("empty-init-relayer")), u256.NewUint(5), order, false)
233+
234+
uassert.NoError(t, err)
235+
uassert.True(t, res.Status == core.PacketStatusSuccess)
236+
237+
v := getVoucher(ibcDenom)
238+
uassert.True(t, v != nil)
239+
uassert.Equal(t, 0, v.token.GetDecimals())
169240
}
170241

171242
func TestTokenOrderExecuteInitializeMintErrorDoesNotSetOrigin(cur realm, t *testing.T) {
@@ -227,7 +298,7 @@ func TestTokenOrderIntentVoucherMarketMakerFillTransfersMakerFunds(cur realm, t
227298
maker := testAddress("v1-intent-maker")
228299
receiver := testAddress("v1-intent-receiver")
229300
quoteToken := "ibc/v1-intent-quote"
230-
uassert.NoError(t, mintVoucher(cross(cur), quoteToken, "base", [32]byte{}, maker, u256.NewUint(30)))
301+
uassert.NoError(t, mintVoucher(cross(cur), quoteToken, "base", [32]byte{}, "", "", 0, maker, u256.NewUint(30)))
231302
order := z.TokenOrderV2{
232303
Sender: []byte("sender"),
233304
Receiver: []byte(receiver),
@@ -280,7 +351,7 @@ func TestTokenOrderIntentInsufficientMakerBalanceReturnsOnlyMaker(cur realm, t *
280351
maker := testAddress("v1-intent-poor-maker")
281352
receiver := testAddress("int-poor-rx")
282353
quoteToken := "ibc/v1-intent-poor"
283-
uassert.NoError(t, mintVoucher(cross(cur), quoteToken, "base", [32]byte{}, maker, u256.NewUint(3)))
354+
uassert.NoError(t, mintVoucher(cross(cur), quoteToken, "base", [32]byte{}, "", "", 0, maker, u256.NewUint(3)))
284355
order := z.TokenOrderV2{
285356
Receiver: []byte(receiver),
286357
BaseToken: []byte("ugnot"),
@@ -367,6 +438,14 @@ func mustEncodeTokenMetadata(meta z.TokenMetadata) []byte {
367438
return bz
368439
}
369440

441+
func mustEncodeTokenInitializer(ti z.TokenInitializer) []byte {
442+
bz, err := z.EncodeTokenInitializer(ti)
443+
if err != nil {
444+
panic(err)
445+
}
446+
return bz
447+
}
448+
370449
func mustDecodeTokenMetadata(bz []byte) z.TokenMetadata {
371450
meta, err := z.DecodeTokenMetadata(bz)
372451
if err != nil {

0 commit comments

Comments
 (0)