token_order.gno
27.37 Kb · 709 lines
1package ucs03_zkgm
2
3import (
4 types "gno.land/p/onbloc/ibc/union/types"
5
6 "gno.land/p/demo/tokens/grc20"
7 z "gno.land/p/onbloc/ibc/union/zkgm"
8 u256 "gno.land/p/onbloc/math/uint256"
9 "gno.land/r/demo/defi/grc20reg"
10 core "gno.land/r/onbloc/ibc/union/core"
11)
12
13// tagAckSuccess returns a fresh copy of the success ack tag.
14func tagAckSuccess() *u256.Uint { return z.Uint256FromBytes32(z.TAG_ACK_SUCCESS) }
15func tagAckFailure() *u256.Uint { return z.Uint256FromBytes32(z.TAG_ACK_FAILURE) }
16func fillTypeProtocol() *u256.Uint { return z.Uint256FromBytes32(z.FILL_TYPE_PROTOCOL) }
17func fillTypeMarketMaker() *u256.Uint { return z.Uint256FromBytes32(z.FILL_TYPE_MARKETMAKER) }
18
19// verifyTokenOrderV2 validates a token order on send: rate-limit, escrow voucher
20// or consume native funds, then escrow the channel balance.
21// reference: https://github.com/unionlabs/union/blob/d91c5e94354e15801bd5f82dc658eae3b79f2dad/cosmwasm/app/ucs03-zkgm/src/contract.rs#L419-L547
22func (v *ucs03ZkgmV1) verifyTokenOrderV2(_ int, rlm realm, funds *funds, channelId types.ChannelId, path *u256.Uint, order z.TokenOrderV2) error {
23 if path == nil {
24 path = u256.Zero()
25 }
26
27 switch order.Kind {
28 case z.TOKEN_ORDER_KIND_INITIALIZE, z.TOKEN_ORDER_KIND_ESCROW:
29 baseDenom := string(order.BaseToken)
30
31 // Verify the metadata decimals match the origin decimals.
32 if order.Kind == z.TOKEN_ORDER_KIND_INITIALIZE {
33 if err := v.verifyMetadataDecimals(baseDenom, order.Metadata); err != nil {
34 return err
35 }
36 }
37
38 baseTokenLedgerAmount, err := v.ledgerAmountOf(baseDenom, order.BaseAmount)
39 if err != nil {
40 return err
41 }
42
43 // reference verify_token_order_v2 increases the channel balance before
44 // escrowing the funds (contract.rs L511-543); rate limiting happens only
45 // on the receive/execute side, never here.
46 err = v.increaseChannelBalanceV2(0, rlm, channelId, path, order.BaseToken, order.QuoteToken, baseTokenLedgerAmount)
47 if err != nil {
48 return err
49 }
50
51 err = v.localTokenEscrow(0, rlm, baseDenom, rlm.Previous().Address(), funds, order.BaseAmount)
52 if err != nil {
53 // rollback the channel balance
54 v.decreaseChannelBalanceV2(0, rlm, channelId, path, order.BaseToken, order.QuoteToken, baseTokenLedgerAmount)
55
56 return err
57 }
58
59 return nil
60 case z.TOKEN_ORDER_KIND_UNESCROW:
61 baseDenom := string(order.BaseToken)
62
63 origin, ok := v.store.GetTokenOrigin(baseDenom)
64 if !ok {
65 return makeError(errInvalidUnescrow)
66 }
67
68 intermediatePath, destChan := z.PopChannelFromPath(origin)
69
70 reversed, err := z.ReverseChannelPath(intermediatePath)
71 if err != nil {
72 return err
73 }
74
75 if path.Cmp(reversed) != 0 {
76 return makeError(errInvalidUnescrow)
77 }
78
79 if channelId != types.ChannelId(destChan) {
80 return makeError(errInvalidUnescrow)
81 }
82
83 image, ok := v.store.GetMetadataImageOf(baseDenom)
84 if !ok {
85 image = [32]byte{}
86 }
87 // reference predicts against the incoming channel_id (contract.rs L453,
88 // L466), not the popped destination channel. The is_sending_back check
89 // above guarantees they are equal on the valid-unescrow path.
90 wrappedV1 := z.PredictWrappedToken(intermediatePath, uint32(channelId), order.QuoteToken)
91 wrappedV2 := z.PredictWrappedTokenV2(intermediatePath, uint32(channelId), order.QuoteToken, image)
92
93 if baseDenom != wrappedV1 && baseDenom != wrappedV2 {
94 return makeError(errInvalidUnescrow)
95 }
96
97 // UNESCROW base on the valid path is always an ibc/ wrapped voucher (see wrappedV1/V2 check above).
98 // Burn it from the sender:
99 // supply destruction on the source chain is the correct model for IBC vouchers.
100 // GRC20 and native tokens are not valid UNESCROW bases here;
101 // they are escrowed via localTokenEscrow on INITIALIZE/ESCROW instead.
102 if isIBCTokenDenom(baseDenom) {
103 return v.burnVoucher(0, rlm, baseDenom, rlm.Previous().Address(), order.BaseAmount)
104 }
105
106 // Defensive fallback for non-ibc denoms: deduct from tx-attached native funds.
107 // Valid UNESCROW orders never reach this branch.
108 return funds.sub(baseDenom, order.BaseAmount)
109 case z.TOKEN_ORDER_KIND_SOLVE:
110 return makeError(errSolveNotImplemented)
111 default:
112 return makeError(errUnknownTokenOrderKind)
113 }
114}
115
116// executeTokenOrderV2 settles a received token order: mint wrapped vouchers, release escrowed native, or fill from the market maker.
117// reference: https://github.com/unionlabs/union/blob/d91c5e94354e15801bd5f82dc658eae3b79f2dad/cosmwasm/app/ucs03-zkgm/src/contract.rs#L2133-L2306
118func (v *ucs03ZkgmV1) executeTokenOrderV2(_ int, rlm realm, packet types.Packet, relayer address, relayerMsg []byte, salt [32]byte, path *u256.Uint, order z.TokenOrderV2, intent bool) (types.RecvPacketResult, error) {
119 // Intent packets and explicit solve orders are maker-only.
120 if intent || order.Kind == z.TOKEN_ORDER_KIND_SOLVE {
121 return v.marketMakerFillV2(0, rlm, relayer, relayerMsg, order)
122 }
123
124 if path == nil {
125 path = u256.Zero()
126 }
127
128 quoteDenom := string(order.QuoteToken)
129 baseCovers := order.BaseAmount.Cmp(order.QuoteAmount) >= 0
130
131 // Direct unescrow path: an unwrap whose base covers the quote releases the
132 // escrowed native asset to the receiver.
133 if order.Kind == z.TOKEN_ORDER_KIND_UNESCROW && baseCovers {
134 if err := v.rateLimit(0, rlm, quoteDenom, order.QuoteAmount); err != nil {
135 return failureResult(universalErrorAck()), nil
136 }
137
138 return v.protocolFillUnescrowV2(0, rlm, packet.DestinationChannelId, path, order.BaseToken, quoteDenom, address(order.Receiver), relayer, order.BaseAmount, order.QuoteAmount)
139 }
140
141 destChan, err := tokenOrderDestinationChannel(packet.DestinationChannelId)
142 if err != nil {
143 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
144 }
145
146 // Predict the wrapped denom for an escrow/initialize order.
147 // A mismatch (or an under-quote) is not an error:
148 // the order falls back to a market-maker fill.
149 var (
150 wrapped string
151 hasWrapped bool
152 image [32]byte
153 meta z.TokenMetadata
154 metaPresent bool
155 )
156
157 switch order.Kind {
158 case z.TOKEN_ORDER_KIND_ESCROW:
159 if img, ok := v.store.GetMetadataImageOf(quoteDenom); ok && !isZeroImage(img) {
160 image = img
161 wrapped = z.PredictWrappedTokenV2(path, destChan, order.BaseToken, image)
162 } else {
163 image = [32]byte{}
164 wrapped = z.PredictWrappedToken(path, destChan, order.BaseToken)
165 }
166
167 hasWrapped = true
168
169 // ESCROW targets an already-deployed wrapped voucher;
170 // the deploying first transfer of a token uses INITIALIZE (which carries the metadata).
171 // A first-seen ESCROW that matches the predicted wrapped denom has no persisted ledger/scale,
172 // so minting would lazily create the voucher at decimals=0 with no origin metadata:
173 // an unredeemable orphan whose scale then diverges from a later INITIALIZE's pre-flight check.
174 // Reject it as an invalid order so the source refunds, mirroring the INITIALIZE invalid-order handling below.
175 if quoteDenom == wrapped && !v.store.HasVoucher(wrapped) {
176 return failureResult(universalErrorAck()), nil
177 }
178 case z.TOKEN_ORDER_KIND_INITIALIZE:
179 // gno requires the metadata preimage to initialize a new wrapped token.
180 if len(order.Metadata) == 0 {
181 return failureResult(universalErrorAck()), nil
182 }
183
184 m, derr := z.DecodeTokenMetadata(order.Metadata)
185 if derr != nil {
186 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), derr
187 }
188
189 meta, metaPresent = m, true
190 image = z.MetadataImage(meta)
191 wrapped = z.PredictWrappedTokenV2(path, destChan, order.BaseToken, image)
192 // A quote token that does not match the deterministic wrapped denom is an
193 // invalid initialize order (union InvalidTokenOrderKind).
194 if quoteDenom != wrapped {
195 return failureResult(universalErrorAck()), nil
196 }
197
198 hasWrapped = true
199 }
200
201 // Protocol fill when the quote matches the predicted wrapped denom and the base
202 // covers the quote; otherwise fall back to a market-maker fill.
203 if hasWrapped && quoteDenom == wrapped && baseCovers {
204 if err := v.rateLimit(0, rlm, quoteDenom, order.QuoteAmount); err != nil {
205 return failureResult(universalErrorAck()), nil
206 }
207
208 canDeploy := order.Kind == z.TOKEN_ORDER_KIND_INITIALIZE
209
210 var metadata *z.TokenMetadata
211 if metaPresent {
212 metadata = &meta
213 }
214
215 return v.protocolFillMint(0, rlm, packet.DestinationChannelId, path, order.BaseToken, wrapped, address(order.Receiver), relayer, order.BaseAmount, order.QuoteAmount, metadata, canDeploy)
216 }
217
218 return v.marketMakerFillV2(0, rlm, relayer, relayerMsg, order)
219}
220
221// protocolFillMint mints the wrapped voucher for an escrow/initialize order:
222// quote to the receiver, fee to the relayer, then records deploy-time wrapped
223// token metadata. State is written only after the mint succeeds so a recovered
224// failure cannot leave orphan origin state.
225// reference: https://github.com/unionlabs/union/blob/d91c5e94354e15801bd5f82dc658eae3b79f2dad/cosmwasm/app/ucs03-zkgm/src/contract.rs#L2309-L2413
226func (v *ucs03ZkgmV1) protocolFillMint(_ int, rlm realm, channelId types.ChannelId, path *u256.Uint, baseToken []byte, wrappedToken string, receiver address, relayer address, baseAmount *u256.Uint, quoteAmount *u256.Uint, metadata *z.TokenMetadata, canDeploy bool) (types.RecvPacketResult, error) {
227 // fee = base - quote; reject a quote that exceeds base instead of wrapping
228 // around to a near-2^256 fee that would only later trip the amount checks.
229 fee, underflow := u256.Zero().SubOverflow(baseAmount, quoteAmount)
230 if underflow {
231 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), makeError(errTokenOrderFeeUnderflow)
232 }
233
234 // A deployable initialize carries the token metadata (name/symbol/decimals);
235 // derive its image and reject out-of-range decimals before any mint.
236 var image [32]byte
237
238 name, symbol := "", ""
239
240 var (
241 decimals uint8
242
243 // Bound-check the amounts against the int64 ledger after downscaling.
244 // A deploying initialize derives the scale from the declared decimals,
245 // an escrow into an existing voucher from its persisted scale.
246 // The mint below re-derives the same scale,
247 // so a non-divisible or oversized amount fails gracefully here before any state write.
248 scaleExp int
249 )
250
251 if canDeploy && metadata != nil {
252 image = z.MetadataImage(*metadata)
253
254 ti, err := z.DecodeTokenInitializer(metadata.Initializer)
255 if err != nil {
256 // Malformed non-empty calldata mirrors an EVM proxy initialization
257 // revert. Gno deliberately rejects an empty initializer too instead
258 // of creating an uninitialized zero-decimal voucher.
259 return failureResult(universalErrorAck()), nil
260 }
261 if ti.Decimals > grc20.MaxDecimals {
262 return failureResult(universalErrorAck()), nil
263 }
264
265 name, symbol, decimals = ti.Name, ti.Symbol, ti.Decimals
266
267 scaleExp = scaleExpForDecimals(decimals)
268 } else {
269 scaleExp = v.scaleExpOf(wrappedToken)
270 }
271
272 if _, err := z.ScaleDownToInt64(quoteAmount, scaleExp); err != nil {
273 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
274 }
275
276 if _, err := z.ScaleDownToInt64(fee, scaleExp); err != nil {
277 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
278 }
279
280 _, hasForeignToken := v.store.GetForeignToken(wrappedToken)
281 shouldDeploy := canDeploy && metadata != nil && !hasForeignToken
282
283 var nextPath *u256.Uint
284
285 if shouldDeploy {
286 np, err := z.UpdateChannelPath(path, uint32(channelId))
287 if err != nil {
288 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
289 }
290
291 nextPath = np
292 }
293
294 if !quoteAmount.IsZero() {
295 if err := v.mintVoucher(0, rlm, wrappedToken, image, name, symbol, decimals, receiver, quoteAmount); err != nil {
296 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
297 }
298 }
299
300 if !fee.IsZero() {
301 if err := v.mintVoucher(0, rlm, wrappedToken, image, name, symbol, decimals, relayer, fee); err != nil {
302 panic(execFatal{err.Error()})
303 }
304 }
305
306 if shouldDeploy {
307 v.store.SetForeignToken(0, rlm, wrappedToken, baseToken)
308 v.store.SetTokenOrigin(0, rlm, wrappedToken, nextPath)
309 v.store.SetMetadataImageOf(0, rlm, wrappedToken, image)
310 }
311
312 ack, err := encodeTokenOrderSuccessAck(fillTypeProtocol(), nil)
313 if err != nil {
314 panic(execFatal{err.Error()})
315 }
316
317 return core.NewRecvPacketResult(types.PacketStatusSuccess, ack), nil
318}
319
320// protocolFillUnescrowV2 releases an escrowed local asset for an unwrap order:
321// decrease the channel balance first, then send the quote to the receiver and
322// the fee to the relayer from the proxy escrow.
323// reference: https://github.com/unionlabs/union/blob/d91c5e94354e15801bd5f82dc658eae3b79f2dad/cosmwasm/app/ucs03-zkgm/src/contract.rs#L2415-L2488
324func (v *ucs03ZkgmV1) protocolFillUnescrowV2(_ int, rlm realm, channelId types.ChannelId, path *u256.Uint, baseToken []byte, quoteTokenStr string, receiver address, relayer address, baseAmount *u256.Uint, quoteAmount *u256.Uint) (types.RecvPacketResult, error) {
325 reversedPath, err := z.ReverseChannelPath(path)
326 if err != nil {
327 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
328 }
329
330 // fee = base - quote; reject a quote that exceeds base instead of wrapping
331 // around to a near-2^256 fee that would only later trip amountInt64.
332 fee, underflow := u256.Zero().SubOverflow(baseAmount, quoteAmount)
333 if underflow {
334 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), makeError(errTokenOrderFeeUnderflow)
335 }
336
337 if _, err := v.ledgerAmountOf(quoteTokenStr, quoteAmount); err != nil {
338 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
339 }
340
341 if _, err := v.ledgerAmountOf(quoteTokenStr, fee); err != nil {
342 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
343 }
344
345 // The released asset is quoteToken, and base/quote/fee share its precision.
346 // The channel balance records the vault (ledger) amount, so derive it from
347 // base; quote/fee are bound-checked against the int64 ledger (0 for native).
348 baseTokenLedgerAmount, err := v.ledgerAmountOf(quoteTokenStr, baseAmount)
349 if err != nil {
350 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
351 }
352
353 // reference decreases the escrowed balance before unescrowing (contract.rs
354 // L2437-2444); decreaseChannelBalanceV2 rejects an underflow internally. The
355 // quote/base tokens are swapped into the balance key as union does (the EVM
356 // quoteToken occupies the baseToken slot and vice versa).
357 err = v.decreaseChannelBalanceV2(0, rlm, channelId, reversedPath, []byte(quoteTokenStr), baseToken, baseTokenLedgerAmount)
358 if err != nil {
359 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
360 }
361
362 if !quoteAmount.IsZero() {
363 if err := v.localTokenUnescrow(0, rlm, quoteTokenStr, receiver, quoteAmount); err != nil {
364 // rollback the channel balance
365 v.increaseChannelBalanceV2(0, rlm, channelId, reversedPath, []byte(quoteTokenStr), baseToken, baseTokenLedgerAmount)
366
367 panic(execFatal{err.Error()})
368 }
369 }
370
371 if !fee.IsZero() {
372 if err := v.localTokenUnescrow(0, rlm, quoteTokenStr, relayer, fee); err != nil {
373 // rollback the channel balance
374 v.increaseChannelBalanceV2(0, rlm, channelId, reversedPath, []byte(quoteTokenStr), baseToken, baseTokenLedgerAmount)
375
376 panic(execFatal{err.Error()})
377 }
378 }
379
380 ack, err := encodeTokenOrderSuccessAck(fillTypeProtocol(), nil)
381 if err != nil {
382 panic(execFatal{err.Error()})
383 }
384
385 return core.NewRecvPacketResult(types.PacketStatusSuccess, ack), nil
386}
387
388// isZeroImage reports whether a metadata image is the zero value, selecting the V1
389// wrapped-token prediction when true.
390func isZeroImage(image [32]byte) bool {
391 return image == [32]byte{}
392}
393
394// encodeTokenOrderSuccessAck builds a success token-order ack with the given fill type.
395func encodeTokenOrderSuccessAck(fillType *u256.Uint, marketMaker []byte) ([]byte, error) {
396 innerAck, err := z.EncodeTokenOrderAck(z.TokenOrderAck{
397 FillType: fillType,
398 MarketMaker: marketMaker,
399 })
400 if err != nil {
401 return nil, err
402 }
403
404 return z.EncodeAck(z.Ack{Tag: tagAckSuccess(), InnerAck: innerAck})
405}
406
407// marketMakerFillV2 fills a token order from the market maker's voucher balance.
408// Native maker funding is intentionally unsupported in v1 because Gno does not
409// yet plumb unsafe.OriginSend() funds through recv/intent dispatch. Non-voucher
410// quotes therefore return ACK_ERR_ONLY_MAKER; OnIntentRecvPacket turns that into
411// an explicit panic so the proven recv path can handle the packet.
412//
413// TODO(market-maker): add funds plumbing and native quote-token fills in the next
414// version, then align this path with Union's LocalTokenMsg handling.
415//
416// reference: https://github.com/unionlabs/union/blob/d91c5e94354e15801bd5f82dc658eae3b79f2dad/cosmwasm/app/ucs03-zkgm/src/contract.rs#L1791-L1861
417func (v *ucs03ZkgmV1) marketMakerFillV2(_ int, rlm realm, relayer address, relayerMsg []byte, order z.TokenOrderV2) (types.RecvPacketResult, error) {
418 quoteDenom := string(order.QuoteToken)
419 if !v.store.HasVoucher(quoteDenom) {
420 // Union can fill local/native quote tokens from the maker's funds. This
421 // implementation cannot observe those funds here yet, so only voucher
422 // quote tokens are eligible for the market-maker fast path.
423 return core.NewRecvPacketResult(types.PacketStatusSuccess, types.CloneBytes(z.ACK_ERR_ONLY_MAKER)), nil
424 }
425
426 if _, err := v.ledgerAmountOf(quoteDenom, order.QuoteAmount); err != nil {
427 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
428 }
429
430 if err := v.transferVoucher(0, rlm, quoteDenom, relayer, address(order.Receiver), order.QuoteAmount); err != nil {
431 return core.NewRecvPacketResult(types.PacketStatusSuccess, types.CloneBytes(z.ACK_ERR_ONLY_MAKER)), nil
432 }
433
434 // market_maker is the relayer-supplied settlement target (union saves
435 // relayer_msg into MARKET_MAKER); fall back to the relayer's own address when
436 // no message was attached.
437 marketMaker := relayerMsg
438 if len(marketMaker) == 0 {
439 marketMaker = []byte(relayer.String())
440 }
441
442 ack, err := encodeTokenOrderSuccessAck(fillTypeMarketMaker(), marketMaker)
443 if err != nil {
444 return core.NewRecvPacketResult(types.PacketStatusUnknown, nil), err
445 }
446
447 return core.NewRecvPacketResult(types.PacketStatusSuccess, ack), nil
448}
449
450// acknowledgeTokenOrderV2 settles the ack: refund on failure, or release escrow to the market maker on a maker fill.
451// reference: https://github.com/unionlabs/union/blob/d91c5e94354e15801bd5f82dc658eae3b79f2dad/cosmwasm/app/ucs03-zkgm/src/contract.rs#L974-L1072
452func (v *ucs03ZkgmV1) acknowledgeTokenOrderV2(_ int, rlm realm, packet types.Packet, relayer address, salt [32]byte, path *u256.Uint, order z.TokenOrderV2, orderAck z.TokenOrderAck, hasOrderAck bool) error {
453 sourceChannelId := packet.SourceChannelId
454 if !hasOrderAck {
455 return v.refundV2(0, rlm, path, sourceChannelId, order)
456 }
457
458 if orderAck.FillType.Eq(fillTypeProtocol()) {
459 return nil
460 }
461
462 if !orderAck.FillType.Eq(fillTypeMarketMaker()) {
463 return makeError(errUnknownFillType)
464 }
465
466 marketMaker := address(orderAck.MarketMaker)
467
468 switch order.Kind {
469 case z.TOKEN_ORDER_KIND_UNESCROW:
470 return v.refundIBCVoucher(0, rlm, order.BaseToken, marketMaker, order.BaseAmount)
471 case z.TOKEN_ORDER_KIND_INITIALIZE, z.TOKEN_ORDER_KIND_ESCROW:
472 // union: when the maker designates the canonical zero burn address, the
473 // escrowed base is burned instead of released to the maker
474 // (acknowledge_token_order_v2, contract.rs L1030-1059).
475 if marketMaker == z.BURN_ADDRESS {
476 return v.burnEscrowedV2(0, rlm, sourceChannelId, path, order)
477 }
478
479 return v.settleEscrowedV2(0, rlm, sourceChannelId, path, order, marketMaker)
480 default:
481 return makeError(errUnsupportedTokenOrderKindForAck)
482 }
483}
484
485// refundV2 returns a failed or timed-out token order's funds to the sender.
486// reference: https://github.com/unionlabs/union/blob/d91c5e94354e15801bd5f82dc658eae3b79f2dad/cosmwasm/app/ucs03-zkgm/src/contract.rs#L918-L971
487func (v *ucs03ZkgmV1) refundV2(_ int, rlm realm, path *u256.Uint, sourceChannelId types.ChannelId, order z.TokenOrderV2) error {
488 switch order.Kind {
489 case z.TOKEN_ORDER_KIND_UNESCROW:
490 return v.refundIBCVoucher(0, rlm, order.BaseToken, address(order.Sender), order.BaseAmount)
491 case z.TOKEN_ORDER_KIND_INITIALIZE, z.TOKEN_ORDER_KIND_ESCROW:
492 return v.settleEscrowedV2(0, rlm, sourceChannelId, path, order, address(order.Sender))
493 default:
494 return makeError(errUnsupportedTokenOrderKindForRefund)
495 }
496}
497
498// settleEscrowedV2 releases a previously escrowed INITIALIZE/ESCROW BaseAmount to
499// recipient: it transfers `ibc/` vouchers from the proxy escrow address and
500// releases native tokens from the proxy escrow otherwise. Shared by refundV2 and
501// the market-maker fill branch of acknowledgeTokenOrderV2.
502func (v *ucs03ZkgmV1) settleEscrowedV2(_ int, rlm realm, sourceChannelId types.ChannelId, path *u256.Uint, order z.TokenOrderV2, recipient address) error {
503 // Base is released as the local baseToken; the channel balance records the
504 // vault (ledger) amount for that denom (0-scale for native), matching
505 // localTokenUnescrow.
506 baseDenom := string(order.BaseToken)
507
508 baseTokenLedgerAmount, err := v.ledgerAmountOf(baseDenom, order.BaseAmount)
509 if err != nil {
510 return err
511 }
512
513 // reference decreases the escrowed balance before unescrowing
514 // (refund_v2 contract.rs L894-902 / ack L1021-1028);
515 // decreaseChannelBalanceV2 rejects an underflow internally.
516 err = v.decreaseChannelBalanceV2(0, rlm, sourceChannelId, path, order.BaseToken, order.QuoteToken, baseTokenLedgerAmount)
517 if err != nil {
518 return err
519 }
520
521 err = v.localTokenUnescrow(0, rlm, baseDenom, recipient, order.BaseAmount)
522 if err != nil {
523 // rollback the channel balance
524 v.increaseChannelBalanceV2(0, rlm, sourceChannelId, path, order.BaseToken, order.QuoteToken, baseTokenLedgerAmount)
525
526 return err
527 }
528
529 return nil
530}
531
532// burnEscrowedV2 removes the escrowed channel balance and finalizes the base
533// token as burned instead of releasing it to the maker (union burn-address ack).
534// Token-type handling:
535// - IBC voucher (ibc/): supply-burned via burnVoucher from proxy escrow.
536// - Native coin: supply cannot be destroyed; coins stay in the proxy banker
537// balance and are tracked in locked-base accounting instead of the
538// channel escrow balance, since burn settlement is a terminal
539// disposition (like release/refund), not an ongoing escrow.
540// - GRC20: same as native — no mint/burn capability over foreign tokens;
541// balance stays at the proxy and is tracked in locked-base accounting.
542func (v *ucs03ZkgmV1) burnEscrowedV2(_ int, rlm realm, sourceChannelId types.ChannelId, path *u256.Uint, order z.TokenOrderV2) error {
543 // Channel balance records the vault (ledger) amount for the base denom.
544 baseDenom := string(order.BaseToken)
545 tokenType := getTokenType(baseDenom)
546
547 baseTokenLedgerAmount, err := v.ledgerAmountOf(baseDenom, order.BaseAmount)
548 if err != nil {
549 return err
550 }
551
552 err = v.decreaseChannelBalanceV2(0, rlm, sourceChannelId, path, order.BaseToken, order.QuoteToken, baseTokenLedgerAmount)
553 if err != nil {
554 return err
555 }
556
557 switch tokenType {
558 case IBC_TOKEN:
559 if err := v.burnVoucher(0, rlm, baseDenom, rlm.Address(), order.BaseAmount); err != nil {
560 // rollback the channel balance
561 v.increaseChannelBalanceV2(0, rlm, sourceChannelId, path, order.BaseToken, order.QuoteToken, baseTokenLedgerAmount)
562
563 return err
564 }
565 case NATIVE_COIN:
566 // Supply cannot be destroyed: permanently lock the proxy-held balance and
567 // remove it from channel escrow accounting.
568 v.store.AddLockedTokenAmount(0, rlm, baseDenom, order.BaseAmount)
569 case GRC20_TOKEN:
570 // No mint/burn capability over a foreign GRC20: same lock accounting as
571 // native.
572 v.store.AddLockedTokenAmount(0, rlm, baseDenom, order.BaseAmount)
573 }
574
575 return nil
576}
577
578// localTokenEscrow takes custody of the base token for a local INITIALIZE/ESCROW
579// send. Classification mirrors getTokenType:
580// - IBC voucher: transfer into proxy escrow (no burn on outbound escrow).
581// - GRC20: TransferFrom sender into proxy; requires prior approval.
582// - Native: deduct from tx-attached -send funds held in the funds budget.
583func (v *ucs03ZkgmV1) localTokenEscrow(_ int, rlm realm, denom string, sender address, funds *funds, amount *u256.Uint) error {
584 switch getTokenType(denom) {
585 case IBC_TOKEN:
586 return v.transferVoucher(0, rlm, denom, sender, rlm.Address(), amount)
587 case GRC20_TOKEN:
588 return v.localGRC20Escrow(0, rlm, grc20reg.Get(denom), sender, amount)
589 case NATIVE_COIN:
590 return funds.sub(denom, amount)
591 }
592
593 return nil
594}
595
596// localTokenUnescrow releases custody of the base token (recv unwrap / refund /
597// settleEscrowedV2). Classification mirrors localTokenEscrow:
598// - IBC voucher: transfer from proxy escrow to recipient (not a mint).
599// - GRC20: Transfer from proxy custody to recipient.
600// - Native: sendNative from the proxy banker balance.
601func (v *ucs03ZkgmV1) localTokenUnescrow(_ int, rlm realm, denom string, recipient address, amount *u256.Uint) error {
602 switch getTokenType(denom) {
603 case IBC_TOKEN:
604 return v.transferVoucher(0, rlm, denom, rlm.Address(), recipient, amount)
605 case GRC20_TOKEN:
606 return v.localGRC20Unescrow(0, rlm, grc20reg.Get(denom), recipient, amount)
607 case NATIVE_COIN:
608 return v.sendNative(0, rlm, denom, recipient, amount)
609 }
610
611 return nil
612}
613
614// refundIBCVoucher re-mints an IBC voucher to recipient, preserving the stored metadata image.
615func (v *ucs03ZkgmV1) refundIBCVoucher(_ int, rlm realm, baseToken []byte, recipient address, amount *u256.Uint) error {
616 baseDenom := string(baseToken)
617 image, _ := v.store.GetMetadataImageOf(baseDenom)
618
619 return v.mintVoucher(0, rlm, baseDenom, image, "", "", 0, recipient, amount)
620}
621
622// localGRC20Escrow takes custody of a locally-registered grc20 token (not a
623// zkgm-issued voucher) for a local send. Unlike native's -send, custody moves
624// via TransferFrom, so the sender must have pre-approved this realm.
625func (v *ucs03ZkgmV1) localGRC20Escrow(_ int, rlm realm, token *grc20.Token, sender address, amount *u256.Uint) error {
626 n, err := amountInt64(amount)
627 if err != nil {
628 return err
629 }
630
631 teller := token.RealmTeller(0, rlm)
632
633 beforeBalance := teller.BalanceOf(rlm.Address())
634
635 err = teller.TransferFrom(0, rlm, sender, rlm.Address(), n)
636 if err != nil {
637 return err
638 }
639
640 afterBalance := teller.BalanceOf(rlm.Address())
641
642 if afterBalance-beforeBalance != n {
643 return makeError(errInvalidTokenOrderTransferAmount)
644 }
645
646 return nil
647}
648
649// localGRC20Unescrow releases a locally-registered grc20 token from proxy
650// custody (recv unwrap / refund), mirroring localGRC20Escrow.
651func (v *ucs03ZkgmV1) localGRC20Unescrow(_ int, rlm realm, token *grc20.Token, recipient address, amount *u256.Uint) error {
652 n, err := amountInt64(amount)
653 if err != nil {
654 return err
655 }
656
657 teller := token.RealmTeller(0, rlm)
658
659 beforeBalance := teller.BalanceOf(rlm.Address())
660
661 err = teller.Transfer(0, rlm, recipient, n)
662 if err != nil {
663 return err
664 }
665
666 afterBalance := teller.BalanceOf(rlm.Address())
667
668 if beforeBalance-afterBalance != n {
669 return makeError(errInvalidTokenOrderTransferAmount)
670 }
671
672 return nil
673}
674
675// verifyMetadataDecimals rejects an INITIALIZE order whose declared metadata
676// decimals do not match the locally-known ground truth for baseDenom: a
677// registered grc20's real decimals, or a held voucher's recorded
678// OriginDecimals when forwarding it onward. baseDenom that is native (or
679// otherwise unknown here) is out of scope for this check. Left unchecked, a
680// wrong declaration would still move the correct raw amount here but cause
681// the counterparty (or next hop) to deploy/interpret the wrapped copy at the
682// wrong precision.
683func (v *ucs03ZkgmV1) verifyMetadataDecimals(baseDenom string, metadata []byte) error {
684 var originDecimals uint8
685
686 if token := grc20reg.Get(baseDenom); token != nil {
687 originDecimals = uint8(token.GetDecimals())
688 } else if vou, ok := v.store.GetVoucher(baseDenom); ok {
689 originDecimals = vou.OriginDecimals()
690 } else {
691 return nil
692 }
693
694 meta, err := z.DecodeTokenMetadata(metadata)
695 if err != nil {
696 return err
697 }
698
699 ti, err := z.DecodeTokenInitializer(meta.Initializer)
700 if err != nil {
701 return err
702 }
703
704 if ti.Decimals != originDecimals {
705 return makeError(errMetadataDecimalsMismatch)
706 }
707
708 return nil
709}