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

app.gno

15.25 Kb · 465 lines
  1package transfer
  2
  3import (
  4	"bytes"
  5	"chain"
  6	"chain/runtime/unsafe"
  7	"errors"
  8	"strconv"
  9	"strings"
 10
 11	"gno.land/p/aib/ibc/app"
 12	"gno.land/p/aib/ibc/types"
 13	"gno.land/p/nt/ufmt/v0"
 14	"gno.land/r/aib/ibc/core"
 15	"gno.land/r/demo/defi/grc20reg"
 16)
 17
 18type App struct{}
 19
 20const (
 21	// NOTE we must use the same portID as the ibc-go transfer IBC app
 22	PortID = "transfer"
 23	// V1 defines first version of the IBC transfer module
 24	V1               = "ics20-1"
 25	EncodingProtobuf = "application/x-protobuf"
 26
 27	denomPrefix          = "ibc"
 28	escrowAddressVersion = V1
 29)
 30
 31func init(cur realm) {
 32	// Register the app in the IBC router.
 33	core.RegisterApp(cross(cur), PortID, &App{})
 34}
 35
 36var _ app.IBCApp = &App{}
 37
 38// Implements app.IBCApp
 39func (a *App) OnSendPacket(
 40	cur realm,
 41	sourceClient string,
 42	destinationClient string,
 43	sequence uint64,
 44	payload types.Payload,
 45) error {
 46	// TODO add parameter to disable the app
 47	// TODO add parameter to block sender addr
 48
 49	// Enforce that the source and destination portIDs are the same and equal to
 50	// the transfer portID.
 51	// Enforce that the source and destination clientIDs are also in the clientID
 52	// format that transfer expects: {clientid}-{sequence}.
 53	// This is necessary for IBC v2 since the portIDs (and thus the
 54	// application-application connection) is not prenegotiated by the channel
 55	// handshake.
 56	// This restriction can be removed in a future where the trace hop on receive
 57	// commits to **both** the source and destination portIDs rather than just
 58	// the destination port.
 59	if payload.SourcePort != PortID || payload.DestinationPort != PortID {
 60		return ufmt.Errorf("payload port ID is invalid: expected %s, got sourcePort: %s destPort: %s", PortID, payload.SourcePort, payload.DestinationPort)
 61	}
 62	if !types.IsValidClientID(sourceClient) || !types.IsValidClientID(destinationClient) {
 63		return ufmt.Errorf("client IDs must be in valid format: {string}-{number}")
 64	}
 65	if payload.Version != V1 {
 66		return ufmt.Errorf("invalid ICS20 version: expected %s, got %s", V1, payload.Version)
 67	}
 68	if payload.Encoding != EncodingProtobuf {
 69		return ufmt.Errorf("invalid encoding: expected %s, got %s", EncodingProtobuf, payload.Encoding)
 70	}
 71
 72	data, token, err := unmarshalPayload(payload.Value)
 73	if err != nil {
 74		return err
 75	}
 76
 77	// Mirror transfer.transfer(): the packet Sender is the EOA that signed
 78	// the tx. unsafe.OriginCaller is tx-level identity (no cur equivalent).
 79	signer := unsafe.OriginCaller().String()
 80	if data.Sender != signer {
 81		return ufmt.Errorf("invalid FungibleTokenPacketData: sender %s is different from signer %s", data.Sender, signer)
 82	}
 83
 84	// Enforce that the base denom does not contain any slashes
 85	// Since IBC v2 packets will no longer have channel identifiers, we cannot
 86	// rely on the channel format to easily divide the trace from the base
 87	// denomination in ICS20 v1 packets.
 88	// The simplest way to prevent any potential issues from arising is to simply
 89	// disallow any slashes in the base denomination.
 90	// This prevents such denominations from being sent with IBCV v2 packets,
 91	// however we can still support them in IBC v1 packets.
 92	// If we enforce that IBC v2 packets are sent with ICS20 v2 and above
 93	// versions that separate the trace from the base denomination in the packet
 94	// data, then we can remove this restriction.
 95	// Non-IBC GRC20 tokens use aliases (slashes replaced with colons)
 96	// so they pass this check naturally.
 97	if err := validateBaseDenomNoSlash(token); err != nil {
 98		return err
 99	}
100
101	coin, err := token.ToCoin()
102	if err != nil {
103		return ufmt.Errorf("token to coin error: %v", err)
104	}
105	if token.Denom.HasPrefix(payload.SourcePort, sourceClient) {
106		if err := consumePendingVoucherSend(
107			data.Sender,
108			sourceClient,
109			token.Denom.Path(),
110			coin.Amount,
111		); err != nil {
112			return err
113		}
114
115		// Burn the voucher tokens from sender
116		inst := getVoucher(coin.Denom)
117		if inst == nil {
118			return ufmt.Errorf("voucher token not found for denom %s", coin.Denom)
119		}
120		if err := inst.ledger.Burn(address(data.Sender), coin.Amount); err != nil {
121			return ufmt.Errorf("burn voucher %s error: %v", coin.String(), err)
122		}
123	} else if isGRC20Alias(token.Denom.Base) {
124		if err := consumePendingGRC20Send(
125			data.Sender,
126			sourceClient,
127			token.Denom.Path(),
128			coin.Amount,
129		); err != nil {
130			return err
131		}
132
133		// Non-IBC GRC20 token: escrow via TransferFrom.
134		// The caller must have approved the transfer app realm address.
135		denomKey := resolveGRC20Alias(token.Denom.Base)
136		grc20Token := grc20reg.Get(denomKey)
137		if grc20Token == nil {
138			return ufmt.Errorf("GRC20 token %s not found in grc20reg", denomKey)
139		}
140		teller := grc20Token.RealmTeller(0, cur)
141		if err := teller.TransferFrom(0, cur, address(data.Sender), cur.Address(), coin.Amount); err != nil {
142			return ufmt.Errorf("escrow GRC20 %s error: %v", denomKey, err)
143		}
144		addEscrowForDenom(coin)
145	} else {
146		// Native token: OnSendPacket cannot safely read banker.OriginSend()
147		// here because PreviousRealm() is the core realm (not the EOA), so
148		// the escrow envelope is verified upstream by Transfer() and handed
149		// off via pendingNativeEscrow. A direct caller of core.SendPacket
150		// for a native packet will find the slot nil and be rejected.
151		// Transfer's defer is responsible for clearing the slot.
152		if pendingNativeEscrow == nil {
153			return ufmt.Errorf("native packet must be initiated through transfer.Transfer")
154		}
155		expected := *pendingNativeEscrow
156		if coin.Denom != expected.Denom || coin.Amount != expected.Amount {
157			return ufmt.Errorf(
158				"escrowed coin %s is not equal to fungible packet data token %s",
159				expected, coin,
160			)
161		}
162		// Escrow the coin on realm balance
163		addEscrowForDenom(coin)
164	}
165
166	// Emit events
167	chain.Emit(EventTypeTransfer,
168		AttributeKeySender, data.Sender,
169		AttributeKeyReceiver, data.Receiver,
170		AttributeKeyDenom, token.Denom.Path(),
171		AttributeKeyAmount, token.Amount,
172		AttributeKeyMemo, data.Memo,
173	)
174	return nil
175}
176
177func consumePendingVoucherSend(sender, sourceClient, denom string, amount int64) error {
178	expected := pendingVoucherSend
179	if expected == nil {
180		return ufmt.Errorf("voucher packet must be initiated through transfer.Transfer")
181	}
182	pendingVoucherSend = nil
183
184	if err := validatePendingSend(expected, sender, sourceClient, denom, amount); err != nil {
185		return ufmt.Errorf("voucher packet %v", err)
186	}
187	return nil
188}
189
190func consumePendingGRC20Send(sender, sourceClient, denom string, amount int64) error {
191	expected := pendingGRC20Send
192	if expected == nil {
193		return ufmt.Errorf("GRC20 packet must be initiated through transfer.Transfer")
194	}
195	pendingGRC20Send = nil
196
197	if err := validatePendingSend(expected, sender, sourceClient, denom, amount); err != nil {
198		return ufmt.Errorf("GRC20 packet %v", err)
199	}
200	return nil
201}
202
203func validatePendingSend(expected *pendingSend, sender, sourceClient, denom string, amount int64) error {
204	if expected.sender != sender ||
205		expected.sourceClient != sourceClient ||
206		expected.denom != denom ||
207		expected.amount != amount {
208		return ufmt.Errorf("does not match transfer.Transfer")
209	}
210	return nil
211}
212
213// Implements app.IBCApp
214func (a *App) OnRecvPacket(
215	cur realm,
216	sourceClient string,
217	destinationClient string,
218	sequence uint64,
219	payload types.Payload,
220) types.RecvPacketResult {
221	// TODO add parameter to disable the app
222	// TODO add parameter to block receiver addr
223
224	// Enforce that the source and destination portIDs are the same and equal to
225	// the transfer portID.
226	// Enforce that the source and destination clientIDs are also in the clientID
227	// format that transfer expects: {clientid}-{sequence}.
228	// This is necessary for IBC v2 since the portIDs (and thus the
229	// application-application connection) is not prenegotiated by the channel
230	// handshake.
231	// This restriction can be removed in a future where the trace hop on receive
232	// commits to **both** the source and destination portIDs rather than just
233	// the destination port.
234	if payload.SourcePort != PortID || payload.DestinationPort != PortID {
235		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
236	}
237	if !types.IsValidClientID(sourceClient) || !types.IsValidClientID(destinationClient) {
238		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
239	}
240	if payload.Version != V1 {
241		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
242	}
243	if payload.Encoding != EncodingProtobuf {
244		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
245	}
246
247	var (
248		data       FungibleTokenPacketData
249		token      Token
250		ackErr     error
251		ack        = types.NewResultAppAcknowledgement([]byte{byte(1)})
252		recvResult = types.RecvPacketResult{
253			Status:          types.PacketStatus_Success,
254			Acknowledgement: ack.MarshalJSON(),
255		}
256	)
257	// we are explicitly wrapping this emit event call in an anonymous function
258	// so that the packet data is evaluated after it has been assigned a value.
259	defer func() {
260		attrs := []string{
261			AttributeKeySender, data.Sender,
262			AttributeKeyReceiver, data.Receiver,
263			AttributeKeyDenom, token.Denom.Path(),
264			AttributeKeyAmount, token.Amount,
265			AttributeKeyMemo, data.Memo,
266			AttributeKeyAckSuccess, strconv.FormatBool(ack.Success()),
267		}
268		if ackErr != nil {
269			attrs = append(attrs,
270				[]string{AttributeKeyAckError, ackErr.Error()}...,
271			)
272		}
273		chain.Emit(EventTypePacket, attrs...)
274	}()
275
276	data, token, ackErr = unmarshalPayload(payload.Value)
277	if ackErr != nil {
278		ack = types.NewErrorAppAcknowledgement(ackErr)
279		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
280	}
281	if ackErr = validateBaseDenomNoSlash(token); ackErr != nil {
282		ack = types.NewErrorAppAcknowledgement(ackErr)
283		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
284	}
285	// This is the prefix that would have been prefixed to the denomination
286	// on sender chain IF and only if the token originally came from the
287	// receiving chain.
288	//
289	// NOTE: We use SourcePort and SourceClient here, because the counterparty
290	// chain would have prefixed with DestPort and DestClient when originally
291	// receiving this token.
292	if token.Denom.HasPrefix(payload.SourcePort, sourceClient) {
293		// sender chain is not the source, unescrow tokens
294
295		// remove prefix added by sender chain
296		token.Denom.Trace = token.Denom.Trace[1:]
297		var transferAmount int64
298		transferAmount, ackErr = token.AmountInt64()
299		if ackErr != nil {
300			ack = types.NewErrorAppAcknowledgement(ackErr)
301			return types.RecvPacketResult{Status: types.PacketStatus_Failure}
302		}
303
304		coin := chain.NewCoin(token.Denom.IBCDenom(), transferAmount)
305
306		if isGRC20Alias(coin.Denom) {
307			ackErr = unescrowGRC20(0, cur, data.Receiver, coin)
308		} else {
309			ackErr = unescrowNative(0, cur, data.Receiver, coin)
310		}
311		if ackErr != nil {
312			ack = types.NewErrorAppAcknowledgement(ackErr)
313			return types.RecvPacketResult{Status: types.PacketStatus_Failure}
314		}
315
316	} else {
317		// sender chain is the source, mint vouchers
318
319		// since SendPacket did not prefix the denomination, we must add the
320		// destination port and client to the trace
321		trace := []Hop{NewHop(payload.DestinationPort, destinationClient)}
322		token.Denom.Trace = append(trace, token.Denom.Trace...)
323
324		voucherDenom := token.Denom.IBCDenom()
325		if !hasDenom(voucherDenom) {
326			setDenom(token.Denom)
327		}
328
329		chain.Emit(EventTypeDenom,
330			AttributeKeyDenomHash, token.Denom.HashHex(),
331			AttributeKeyDenom, string(token.Denom.MarshalJSON()),
332		)
333
334		// Mint voucher tokens to the receiver
335		var amount int64
336		amount, ackErr = token.AmountInt64()
337		if ackErr != nil {
338			ack = types.NewErrorAppAcknowledgement(ackErr)
339			return types.RecvPacketResult{Status: types.PacketStatus_Failure}
340		}
341		inst := getOrCreateVoucher(0, cur, token.Denom.Base, voucherDenom)
342		if err := inst.ledger.Mint(address(data.Receiver), amount); err != nil {
343			ackErr = ufmt.Errorf("mint voucher error: %v", err)
344			ack = types.NewErrorAppAcknowledgement(ackErr)
345			return types.RecvPacketResult{Status: types.PacketStatus_Failure}
346		}
347	}
348
349	return recvResult
350}
351
352// validateBaseDenomNoSlash rejects base denominations containing a slash.
353// IBC v2 packets do not carry channel identifiers, so denomination traces
354// cannot be unambiguously separated from a slashed base denomination.
355func validateBaseDenomNoSlash(token Token) error {
356	if strings.Contains(token.Denom.Base, "/") {
357		return ufmt.Errorf("base denomination %s cannot contain slashes for IBC v2 packet", token.Denom.Base)
358	}
359	return nil
360}
361
362// OnAcknowledgementPacket responds to the success or failure of a packet
363// acknowledgment written on the receiving chain.
364//
365// If the acknowledgement was a success then nothing occurs. Otherwise,
366// if the acknowledgement failed, then the sender is refunded their tokens.
367// Implements app.IBCApp
368func (a *App) OnAcknowledgementPacket(
369	cur realm,
370	sourceClient string,
371	destinationClient string,
372	sequence uint64,
373	acknowledgement []byte,
374	payload types.Payload,
375) error {
376	var ack types.AppAcknowledgement
377	// Construct an error acknowledgement if the acknowledgement bytes are the
378	// sentinel error acknowledgement so we can use the shared transfer logic
379	if bytes.Equal(acknowledgement, types.UniversalErrorAcknowledgement()) {
380		// the specific error does not matter
381		ack = types.NewErrorAppAcknowledgement(errors.New("receive packet failed"))
382	} else {
383		if err := ack.UnmarshalJSON(acknowledgement); err != nil {
384			return ufmt.Errorf("cannot unmarshal ICS-20 transfer packet acknowledgement: %v", err)
385		}
386		if !ack.Success() {
387			return ufmt.Errorf("cannot pass in a custom error acknowledgement with IBC v2")
388		}
389	}
390
391	data, token, err := unmarshalPayload(payload.Value)
392	if err != nil {
393		return err
394	}
395
396	if ack.Success() {
397		// the acknowledgement succeeded on the receiving chain so nothing
398		// needs to be executed and no error needs to be returned
399	} else {
400		// refund sender in case of ack error
401		if err := refundPacketToken(0, cur, payload.SourcePort, sourceClient, data.Sender, token); err != nil {
402			return err
403		}
404	}
405
406	// Emit events
407	chain.Emit(EventTypePacket,
408		AttributeKeySender, data.Sender,
409		AttributeKeyReceiver, data.Receiver,
410		AttributeKeyDenom, token.Denom.Path(),
411		AttributeKeyAmount, token.Amount,
412		AttributeKeyMemo, data.Memo,
413		AttributeKeyAck, string(acknowledgement),
414	)
415	if ack.Success() {
416		chain.Emit(EventTypePacket,
417			AttributeKeyAckSuccess, string(ack.Response.Result),
418		)
419	} else {
420		chain.Emit(EventTypePacket,
421			AttributeKeyAckError, ack.Response.Error,
422		)
423	}
424	return nil
425}
426
427// OnTimeoutPacket processes a transfer packet timeout by refunding the tokens
428// to the sender
429// Implements app.IBCApp
430func (a *App) OnTimeoutPacket(
431	cur realm,
432	sourceClient string,
433	destinationClient string,
434	sequence uint64,
435	payload types.Payload,
436) error {
437	data, token, err := unmarshalPayload(payload.Value)
438	if err != nil {
439		return err
440	}
441	if err := refundPacketToken(0, cur, payload.SourcePort, sourceClient, data.Sender, token); err != nil {
442		return err
443	}
444	// Emit events
445	chain.Emit(EventTypeTimeout,
446		AttributeKeyReceiver, data.Sender,
447		AttributeKeyDenom, token.Denom.Path(),
448		AttributeKeyAmount, token.Amount,
449		AttributeKeyMemo, data.Memo,
450	)
451	return nil
452}
453
454func unmarshalPayload(bz []byte) (FungibleTokenPacketData, Token, error) {
455	var data FungibleTokenPacketData
456	if err := data.ProtoUnmarshal(bz); err != nil {
457		return data, Token{}, ufmt.Errorf("decoding FungibleTokenPacketData: %v", err)
458	}
459	if err := data.ValidateBasic(); err != nil {
460		return data, Token{}, ufmt.Errorf("invalid FungibleTokenPacketData: %v", err)
461	}
462	denom := ExtractDenomFromPath(data.Denom)
463	token := Token{Denom: denom, Amount: data.Amount}
464	return data, token, nil
465}