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}