app.gno
15.73 Kb · 471 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 addEscrowForClient(sourceClient, 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, accounted under sourceClient.
163 addEscrowForClient(sourceClient, 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 // The escrow being released was recorded under destinationClient when
307 // this chain originally sent the token out (the packet now arrives on
308 // destinationClient carrying the counterparty's prefix on the denom).
309 // Debiting destinationClient, not sourceClient, is what prevents a
310 // packet arriving on a different client from draining escrow that
311 // belongs to another client.
312 if isGRC20Alias(coin.Denom) {
313 ackErr = unescrowGRC20(0, cur, destinationClient, data.Receiver, coin)
314 } else {
315 ackErr = unescrowNative(0, cur, destinationClient, data.Receiver, coin)
316 }
317 if ackErr != nil {
318 ack = types.NewErrorAppAcknowledgement(ackErr)
319 return types.RecvPacketResult{Status: types.PacketStatus_Failure}
320 }
321
322 } else {
323 // sender chain is the source, mint vouchers
324
325 // since SendPacket did not prefix the denomination, we must add the
326 // destination port and client to the trace
327 trace := []Hop{NewHop(payload.DestinationPort, destinationClient)}
328 token.Denom.Trace = append(trace, token.Denom.Trace...)
329
330 voucherDenom := token.Denom.IBCDenom()
331 if !hasDenom(voucherDenom) {
332 setDenom(token.Denom)
333 }
334
335 chain.Emit(EventTypeDenom,
336 AttributeKeyDenomHash, token.Denom.HashHex(),
337 AttributeKeyDenom, string(token.Denom.MarshalJSON()),
338 )
339
340 // Mint voucher tokens to the receiver
341 var amount int64
342 amount, ackErr = token.AmountInt64()
343 if ackErr != nil {
344 ack = types.NewErrorAppAcknowledgement(ackErr)
345 return types.RecvPacketResult{Status: types.PacketStatus_Failure}
346 }
347 inst := getOrCreateVoucher(0, cur, token.Denom.Base, voucherDenom)
348 if err := inst.ledger.Mint(address(data.Receiver), amount); err != nil {
349 ackErr = ufmt.Errorf("mint voucher error: %v", err)
350 ack = types.NewErrorAppAcknowledgement(ackErr)
351 return types.RecvPacketResult{Status: types.PacketStatus_Failure}
352 }
353 }
354
355 return recvResult
356}
357
358// validateBaseDenomNoSlash rejects base denominations containing a slash.
359// IBC v2 packets do not carry channel identifiers, so denomination traces
360// cannot be unambiguously separated from a slashed base denomination.
361func validateBaseDenomNoSlash(token Token) error {
362 if strings.Contains(token.Denom.Base, "/") {
363 return ufmt.Errorf("base denomination %s cannot contain slashes for IBC v2 packet", token.Denom.Base)
364 }
365 return nil
366}
367
368// OnAcknowledgementPacket responds to the success or failure of a packet
369// acknowledgment written on the receiving chain.
370//
371// If the acknowledgement was a success then nothing occurs. Otherwise,
372// if the acknowledgement failed, then the sender is refunded their tokens.
373// Implements app.IBCApp
374func (a *App) OnAcknowledgementPacket(
375 cur realm,
376 sourceClient string,
377 destinationClient string,
378 sequence uint64,
379 acknowledgement []byte,
380 payload types.Payload,
381) error {
382 var ack types.AppAcknowledgement
383 // Construct an error acknowledgement if the acknowledgement bytes are the
384 // sentinel error acknowledgement so we can use the shared transfer logic
385 if bytes.Equal(acknowledgement, types.UniversalErrorAcknowledgement()) {
386 // the specific error does not matter
387 ack = types.NewErrorAppAcknowledgement(errors.New("receive packet failed"))
388 } else {
389 if err := ack.UnmarshalJSON(acknowledgement); err != nil {
390 return ufmt.Errorf("cannot unmarshal ICS-20 transfer packet acknowledgement: %v", err)
391 }
392 if !ack.Success() {
393 return ufmt.Errorf("cannot pass in a custom error acknowledgement with IBC v2")
394 }
395 }
396
397 data, token, err := unmarshalPayload(payload.Value)
398 if err != nil {
399 return err
400 }
401
402 if ack.Success() {
403 // the acknowledgement succeeded on the receiving chain so nothing
404 // needs to be executed and no error needs to be returned
405 } else {
406 // refund sender in case of ack error
407 if err := refundPacketToken(0, cur, payload.SourcePort, sourceClient, data.Sender, token); err != nil {
408 return err
409 }
410 }
411
412 // Emit events
413 chain.Emit(EventTypePacket,
414 AttributeKeySender, data.Sender,
415 AttributeKeyReceiver, data.Receiver,
416 AttributeKeyDenom, token.Denom.Path(),
417 AttributeKeyAmount, token.Amount,
418 AttributeKeyMemo, data.Memo,
419 AttributeKeyAck, string(acknowledgement),
420 )
421 if ack.Success() {
422 chain.Emit(EventTypePacket,
423 AttributeKeyAckSuccess, string(ack.Response.Result),
424 )
425 } else {
426 chain.Emit(EventTypePacket,
427 AttributeKeyAckError, ack.Response.Error,
428 )
429 }
430 return nil
431}
432
433// OnTimeoutPacket processes a transfer packet timeout by refunding the tokens
434// to the sender
435// Implements app.IBCApp
436func (a *App) OnTimeoutPacket(
437 cur realm,
438 sourceClient string,
439 destinationClient string,
440 sequence uint64,
441 payload types.Payload,
442) error {
443 data, token, err := unmarshalPayload(payload.Value)
444 if err != nil {
445 return err
446 }
447 if err := refundPacketToken(0, cur, payload.SourcePort, sourceClient, data.Sender, token); err != nil {
448 return err
449 }
450 // Emit events
451 chain.Emit(EventTypeTimeout,
452 AttributeKeyReceiver, data.Sender,
453 AttributeKeyDenom, token.Denom.Path(),
454 AttributeKeyAmount, token.Amount,
455 AttributeKeyMemo, data.Memo,
456 )
457 return nil
458}
459
460func unmarshalPayload(bz []byte) (FungibleTokenPacketData, Token, error) {
461 var data FungibleTokenPacketData
462 if err := data.ProtoUnmarshal(bz); err != nil {
463 return data, Token{}, ufmt.Errorf("decoding FungibleTokenPacketData: %v", err)
464 }
465 if err := data.ValidateBasic(); err != nil {
466 return data, Token{}, ufmt.Errorf("invalid FungibleTokenPacketData: %v", err)
467 }
468 denom := ExtractDenomFromPath(data.Denom)
469 token := Token{Denom: denom, Amount: data.Amount}
470 return data, token, nil
471}