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}