Search Apps Documentation Source Content File Folder Download Copy Actions Download

app.gno

13.33 Kb · 398 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 strings.Contains(token.Denom.Base, "/") {
 98		return ufmt.Errorf("base denomination %s cannot contain slashes for IBC v2 packet", token.Denom.Base)
 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		// Burn the voucher tokens from sender
107		inst := getVoucher(coin.Denom)
108		if inst == nil {
109			return ufmt.Errorf("voucher token not found for denom %s", coin.Denom)
110		}
111		if err := inst.ledger.Burn(address(data.Sender), coin.Amount); err != nil {
112			return ufmt.Errorf("burn voucher %s error: %v", coin.String(), err)
113		}
114	} else if isGRC20Alias(token.Denom.Base) {
115		// Non-IBC GRC20 token: escrow via TransferFrom.
116		// The caller must have approved the transfer app realm address.
117		denomKey := resolveGRC20Alias(token.Denom.Base)
118		grc20Token := grc20reg.Get(denomKey)
119		if grc20Token == nil {
120			return ufmt.Errorf("GRC20 token %s not found in grc20reg", denomKey)
121		}
122		teller := grc20Token.RealmTeller(0, cur)
123		if err := teller.TransferFrom(0, cur, address(data.Sender), cur.Address(), coin.Amount); err != nil {
124			return ufmt.Errorf("escrow GRC20 %s error: %v", denomKey, err)
125		}
126		addEscrowForDenom(coin)
127	} else {
128		// Native token: OnSendPacket cannot safely read banker.OriginSend()
129		// here because PreviousRealm() is the core realm (not the EOA), so
130		// the escrow envelope is verified upstream by Transfer() and handed
131		// off via pendingNativeEscrow. A direct caller of core.SendPacket
132		// for a native packet will find the slot nil and be rejected.
133		// Transfer's defer is responsible for clearing the slot.
134		if pendingNativeEscrow == nil {
135			return ufmt.Errorf("native packet must be initiated through transfer.Transfer")
136		}
137		expected := *pendingNativeEscrow
138		if coin.Denom != expected.Denom || coin.Amount != expected.Amount {
139			return ufmt.Errorf(
140				"escrowed coin %s is not equal to fungible packet data token %s",
141				expected, coin,
142			)
143		}
144		// Escrow the coin on realm balance
145		addEscrowForDenom(coin)
146	}
147
148	// Emit events
149	chain.Emit(EventTypeTransfer,
150		AttributeKeySender, data.Sender,
151		AttributeKeyReceiver, data.Receiver,
152		AttributeKeyDenom, token.Denom.Path(),
153		AttributeKeyAmount, token.Amount,
154		AttributeKeyMemo, data.Memo,
155	)
156	return nil
157}
158
159// Implements app.IBCApp
160func (a *App) OnRecvPacket(
161	cur realm,
162	sourceClient string,
163	destinationClient string,
164	sequence uint64,
165	payload types.Payload,
166) types.RecvPacketResult {
167	// TODO add parameter to disable the app
168	// TODO add parameter to block receiver addr
169
170	// Enforce that the source and destination portIDs are the same and equal to
171	// the transfer portID.
172	// Enforce that the source and destination clientIDs are also in the clientID
173	// format that transfer expects: {clientid}-{sequence}.
174	// This is necessary for IBC v2 since the portIDs (and thus the
175	// application-application connection) is not prenegotiated by the channel
176	// handshake.
177	// This restriction can be removed in a future where the trace hop on receive
178	// commits to **both** the source and destination portIDs rather than just
179	// the destination port.
180	if payload.SourcePort != PortID || payload.DestinationPort != PortID {
181		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
182	}
183	if !types.IsValidClientID(sourceClient) || !types.IsValidClientID(destinationClient) {
184		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
185	}
186	if payload.Version != V1 {
187		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
188	}
189	if payload.Encoding != EncodingProtobuf {
190		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
191	}
192
193	var (
194		data       FungibleTokenPacketData
195		token      Token
196		ackErr     error
197		ack        = types.NewResultAppAcknowledgement([]byte{byte(1)})
198		recvResult = types.RecvPacketResult{
199			Status:          types.PacketStatus_Success,
200			Acknowledgement: ack.MarshalJSON(),
201		}
202	)
203	// we are explicitly wrapping this emit event call in an anonymous function
204	// so that the packet data is evaluated after it has been assigned a value.
205	defer func() {
206		attrs := []string{
207			AttributeKeySender, data.Sender,
208			AttributeKeyReceiver, data.Receiver,
209			AttributeKeyDenom, token.Denom.Path(),
210			AttributeKeyAmount, token.Amount,
211			AttributeKeyMemo, data.Memo,
212			AttributeKeyAckSuccess, strconv.FormatBool(ack.Success()),
213		}
214		if ackErr != nil {
215			attrs = append(attrs,
216				[]string{AttributeKeyAckError, ackErr.Error()}...,
217			)
218		}
219		chain.Emit(EventTypePacket, attrs...)
220	}()
221
222	data, token, ackErr = unmarshalPayload(payload.Value)
223	if ackErr != nil {
224		ack = types.NewErrorAppAcknowledgement(ackErr)
225		return types.RecvPacketResult{Status: types.PacketStatus_Failure}
226	}
227	// This is the prefix that would have been prefixed to the denomination
228	// on sender chain IF and only if the token originally came from the
229	// receiving chain.
230	//
231	// NOTE: We use SourcePort and SourceClient here, because the counterparty
232	// chain would have prefixed with DestPort and DestClient when originally
233	// receiving this token.
234	if token.Denom.HasPrefix(payload.SourcePort, sourceClient) {
235		// sender chain is not the source, unescrow tokens
236
237		// remove prefix added by sender chain
238		token.Denom.Trace = token.Denom.Trace[1:]
239		var transferAmount int64
240		transferAmount, ackErr = token.AmountInt64()
241		if ackErr != nil {
242			ack = types.NewErrorAppAcknowledgement(ackErr)
243			return types.RecvPacketResult{Status: types.PacketStatus_Failure}
244		}
245
246		coin := chain.NewCoin(token.Denom.IBCDenom(), transferAmount)
247
248		if isGRC20Alias(coin.Denom) {
249			ackErr = unescrowGRC20(0, cur, data.Receiver, coin)
250		} else {
251			ackErr = unescrowNative(0, cur, data.Receiver, coin)
252		}
253		if ackErr != nil {
254			ack = types.NewErrorAppAcknowledgement(ackErr)
255			return types.RecvPacketResult{Status: types.PacketStatus_Failure}
256		}
257
258	} else {
259		// sender chain is the source, mint vouchers
260
261		// since SendPacket did not prefix the denomination, we must add the
262		// destination port and client to the trace
263		trace := []Hop{NewHop(payload.DestinationPort, destinationClient)}
264		token.Denom.Trace = append(trace, token.Denom.Trace...)
265
266		voucherDenom := token.Denom.IBCDenom()
267		if !hasDenom(voucherDenom) {
268			setDenom(token.Denom)
269		}
270
271		chain.Emit(EventTypeDenom,
272			AttributeKeyDenomHash, token.Denom.HashHex(),
273			AttributeKeyDenom, string(token.Denom.MarshalJSON()),
274		)
275
276		// Mint voucher tokens to the receiver
277		var amount int64
278		amount, ackErr = token.AmountInt64()
279		if ackErr != nil {
280			ack = types.NewErrorAppAcknowledgement(ackErr)
281			return types.RecvPacketResult{Status: types.PacketStatus_Failure}
282		}
283		inst := getOrCreateVoucher(0, cur, token.Denom.Base, voucherDenom)
284		if err := inst.ledger.Mint(address(data.Receiver), amount); err != nil {
285			ackErr = ufmt.Errorf("mint voucher error: %v", err)
286			ack = types.NewErrorAppAcknowledgement(ackErr)
287			return types.RecvPacketResult{Status: types.PacketStatus_Failure}
288		}
289	}
290
291	return recvResult
292}
293
294// OnAcknowledgementPacket responds to the success or failure of a packet
295// acknowledgment written on the receiving chain.
296//
297// If the acknowledgement was a success then nothing occurs. Otherwise,
298// if the acknowledgement failed, then the sender is refunded their tokens.
299// Implements app.IBCApp
300func (a *App) OnAcknowledgementPacket(
301	cur realm,
302	sourceClient string,
303	destinationClient string,
304	sequence uint64,
305	acknowledgement []byte,
306	payload types.Payload,
307) error {
308	var ack types.AppAcknowledgement
309	// Construct an error acknowledgement if the acknowledgement bytes are the
310	// sentinel error acknowledgement so we can use the shared transfer logic
311	if bytes.Equal(acknowledgement, types.UniversalErrorAcknowledgement()) {
312		// the specific error does not matter
313		ack = types.NewErrorAppAcknowledgement(errors.New("receive packet failed"))
314	} else {
315		if err := ack.UnmarshalJSON(acknowledgement); err != nil {
316			return ufmt.Errorf("cannot unmarshal ICS-20 transfer packet acknowledgement: %v", err)
317		}
318		if !ack.Success() {
319			return ufmt.Errorf("cannot pass in a custom error acknowledgement with IBC v2")
320		}
321	}
322
323	data, token, err := unmarshalPayload(payload.Value)
324	if err != nil {
325		return err
326	}
327
328	if ack.Success() {
329		// the acknowledgement succeeded on the receiving chain so nothing
330		// needs to be executed and no error needs to be returned
331	} else {
332		// refund sender in case of ack error
333		if err := refundPacketToken(0, cur, payload.SourcePort, sourceClient, data.Sender, token); err != nil {
334			return err
335		}
336	}
337
338	// Emit events
339	chain.Emit(EventTypePacket,
340		AttributeKeySender, data.Sender,
341		AttributeKeyReceiver, data.Receiver,
342		AttributeKeyDenom, token.Denom.Path(),
343		AttributeKeyAmount, token.Amount,
344		AttributeKeyMemo, data.Memo,
345		AttributeKeyAck, string(acknowledgement),
346	)
347	if ack.Success() {
348		chain.Emit(EventTypePacket,
349			AttributeKeyAckSuccess, string(ack.Response.Result),
350		)
351	} else {
352		chain.Emit(EventTypePacket,
353			AttributeKeyAckError, ack.Response.Error,
354		)
355	}
356	return nil
357}
358
359// OnTimeoutPacket processes a transfer packet timeout by refunding the tokens
360// to the sender
361// Implements app.IBCApp
362func (a *App) OnTimeoutPacket(
363	cur realm,
364	sourceClient string,
365	destinationClient string,
366	sequence uint64,
367	payload types.Payload,
368) error {
369
370	data, token, err := unmarshalPayload(payload.Value)
371	if err != nil {
372		return err
373	}
374	if err := refundPacketToken(0, cur, payload.SourcePort, sourceClient, data.Sender, token); err != nil {
375		return err
376	}
377	// Emit events
378	chain.Emit(EventTypeTimeout,
379		AttributeKeyReceiver, data.Sender,
380		AttributeKeyDenom, token.Denom.Path(),
381		AttributeKeyAmount, token.Amount,
382		AttributeKeyMemo, data.Memo,
383	)
384	return nil
385}
386
387func unmarshalPayload(bz []byte) (FungibleTokenPacketData, Token, error) {
388	var data FungibleTokenPacketData
389	if err := data.ProtoUnmarshal(bz); err != nil {
390		return data, Token{}, ufmt.Errorf("decoding FungibleTokenPacketData: %v", err)
391	}
392	if err := data.ValidateBasic(); err != nil {
393		return data, Token{}, ufmt.Errorf("invalid FungibleTokenPacketData: %v", err)
394	}
395	denom := ExtractDenomFromPath(data.Denom)
396	token := Token{Denom: denom, Amount: data.Amount}
397	return data, token, nil
398}