transfer.gno
7.91 Kb · 210 lines
1package transfer
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime/unsafe"
7 "strings"
8
9 "gno.land/p/aib/ibc/types"
10 "gno.land/p/nt/ufmt/v0"
11 "gno.land/r/aib/ibc/core"
12 "gno.land/r/demo/defi/grc20reg"
13)
14
15// Transfer is the entry point for sending tokens to another chain using the
16// IBC protocol. It supports three token types, detected automatically from the
17// denom argument:
18// - IBC voucher (denom starts with "ibc/"): burns the voucher GRC20 token.
19// - Non-IBC GRC20 token (registered in grc20reg): escrows via TransferFrom.
20// - Native coin (everything else): the coin must be sent with the transaction
21// via the send field, and must match denom and amount.
22//
23// memo is included in the IBC packet payload.
24func Transfer(cur realm, clientID, receiver, denom string, amount int64, timeoutTimestamp uint64, memo string) (
25 packet types.MsgSendPacket, sequence uint64,
26) {
27 switch {
28 case strings.HasPrefix(denom, "ibc/"):
29 // IBC voucher: validate it exists and resolve the denom path.
30 if getVoucher(denom) == nil {
31 panic(ufmt.Errorf("voucher token not found for denom %s", denom))
32 }
33 d, found := getDenom(denom)
34 if !found {
35 panic(ufmt.Sprintf("denom %s not found in store", denom))
36 }
37 denom = d.Path()
38 case grc20reg.Get(denom) != nil:
39 // Non-IBC GRC20 token: convert to alias.
40 // Escrow via TransferFrom happens in OnSendPacket.
41 // Only catch the "stuck coins" mistake on direct user calls;
42 // when a wrapper realm calls us, OriginSend describes the outer
43 // tx envelope and the coins did not land at this realm.
44 if cur.Previous().IsUserCall() && len(unsafe.OriginSend()) > 0 {
45 panic("GRC20 transfer must not include native coins")
46 }
47 // Replace the denom with a slash-free alias for the IBC packet.
48 // This avoids ibc-go rejecting base denoms containing slashes.
49 denom = GRC20Alias(denom)
50 case strings.Contains(denom, "/"):
51 // This case exists only for a better error message: a denom with
52 // slashes that isn't in grc20reg is likely a mistyped GRC20 path.
53 // Without it, the default (native coin) branch would give a
54 // confusing "requires one non-zero coin to be sent" error.
55 panic(ufmt.Sprintf("GRC20 token %s not found in grc20reg", denom))
56 default:
57 // Native coin: only direct user-call (maketx call) is accepted, so
58 // unsafe.OriginSend() describes coins that actually landed at this
59 // realm. Intermediate code realms or `maketx run` ephemeral realms
60 // can attach -send to the tx but spend it elsewhere.
61 if !cur.Previous().IsUserCall() {
62 panic("native transfer must be a direct user call (maketx call)")
63 }
64 sent := unsafe.OriginSend()
65 if len(sent) != 1 || sent[0].Amount == 0 {
66 panic("native transfer requires one non-zero coin to be sent")
67 }
68 if sent[0].Denom != denom || sent[0].Amount != amount {
69 panic(ufmt.Sprintf(
70 "sent coin %s does not match transfer args (denom=%s, amount=%d)",
71 sent[0], denom, amount,
72 ))
73 }
74 // Hand the verified escrow off to OnSendPacket, which can no longer
75 // safely read OriginSend (its PreviousRealm is the core realm).
76 // The defer below guarantees we clear the slot in any exit path so
77 // a leftover never bleeds into a later call.
78 pendingNativeEscrow = &sent[0]
79 defer func() { pendingNativeEscrow = nil }()
80 }
81 return transfer(cur, clientID, receiver, denom, amount, timeoutTimestamp, memo)
82}
83
84// VoucherSend is a convenience helper to send an IBC voucher token from the
85// caller to the provided recipient.
86func VoucherSend(cur realm, ibcDenom string, to address, amount int64) {
87 inst := getVoucher(ibcDenom)
88 if inst == nil {
89 panic(ufmt.Errorf("voucher token not found for denom %s", ibcDenom))
90 }
91 teller := inst.ledger.ImpersonateTeller(cur.Previous().Address())
92 if err := teller.Transfer(0, cur, to, amount); err != nil {
93 panic(ufmt.Errorf("transfer voucher %s error: %v", ibcDenom, err))
94 }
95}
96
97// VoucherApprove is a convenience helper to set a spender allowance for an IBC
98// voucher token on behalf of the caller.
99func VoucherApprove(cur realm, ibcDenom string, spender address, amount int64) {
100 inst := getVoucher(ibcDenom)
101 if inst == nil {
102 panic(ufmt.Errorf("voucher token not found for denom %s", ibcDenom))
103 }
104 teller := inst.ledger.ImpersonateTeller(cur.Previous().Address())
105 if err := teller.Approve(0, cur, spender, amount); err != nil {
106 panic(ufmt.Errorf("approve voucher %s error: %v", ibcDenom, err))
107 }
108}
109
110func transfer(cur realm, clientID, receiver, denom string, amount int64, timeoutTimestamp uint64, memo string) (
111 packet types.MsgSendPacket, sequence uint64,
112) {
113 // unsafe.OriginCaller is the EOA that signed the tx; we use it as the
114 // packet Sender so Transfer can also be invoked through a wrapper realm.
115 // On the OnSendPacket side the same value is read back to verify the
116 // packet Sender matches the signing EOA.
117 data := NewFungibleTokenPacketData(
118 denom,
119 ufmt.Sprintf("%d", amount),
120 unsafe.OriginCaller().String(),
121 receiver,
122 memo,
123 )
124 packet = types.MsgSendPacket{
125 SourceClient: clientID,
126 Payloads: []types.Payload{{
127 SourcePort: PortID,
128 DestinationPort: PortID,
129 Encoding: EncodingProtobuf,
130 Value: data.ProtoMarshal(),
131 Version: V1,
132 }},
133 TimeoutTimestamp: timeoutTimestamp,
134 }
135 sequence = core.SendPacket(cross(cur), packet)
136 return
137}
138
139// refundPacketToken is a non-crossing helper; rlm is the caller's live cur,
140// needed only by unescrowNative which constructs a Banker.
141func refundPacketToken(_ int, rlm realm, sourcePort, sourceClient, sender string, token Token) error {
142 coin, err := token.ToCoin()
143 if err != nil {
144 return ufmt.Errorf("token to coin error: %v", err)
145 }
146 if token.Denom.HasPrefix(sourcePort, sourceClient) {
147 // Re-mint voucher tokens that were burnt during OnSendPacket
148 inst := getVoucher(coin.Denom)
149 if inst == nil {
150 return ufmt.Errorf("voucher token not found for denom %s", coin.Denom)
151 }
152 if err := inst.ledger.Mint(address(sender), coin.Amount); err != nil {
153 return ufmt.Errorf("re-mint voucher error: %v", err)
154 }
155 } else if isGRC20Alias(coin.Denom) {
156 // Non-IBC GRC20: return escrowed tokens to sender
157 if err := unescrowGRC20(0, rlm, sender, coin); err != nil {
158 return err
159 }
160 } else {
161 // Native: return escrowed coins to sender
162 if err := unescrowNative(0, rlm, sender, coin); err != nil {
163 return err
164 }
165 }
166 return nil
167}
168
169// unescrowGRC20 resolves the GRC20 alias and transfers the escrowed tokens
170// back to the receiver via the token's RealmTeller. Non-crossing helper; rlm
171// is needed by the grc20 teller API.
172func unescrowGRC20(_ int, rlm realm, receiver string, coin chain.Coin) error {
173 denomKey := resolveGRC20Alias(coin.Denom)
174 token := grc20reg.Get(denomKey)
175 if token == nil {
176 return ufmt.Errorf("GRC20 token %s not found in grc20reg", denomKey)
177 }
178 teller := token.RealmTeller(0, rlm)
179 if err := teller.Transfer(0, rlm, address(receiver), coin.Amount); err != nil {
180 return ufmt.Errorf("unescrow GRC20 %s error: %v", denomKey, err)
181 }
182 subEscrowForDenom(coin)
183 return nil
184}
185
186// unescrowNative sends the escrowed native coins back to the receiver via
187// the banker. Non-crossing helper; rlm is the caller's live cur (transfer
188// realm's own frame), needed to construct a Banker.
189func unescrowNative(_ int, rlm realm, receiver string, coin chain.Coin) error {
190 var (
191 // TODO no app/clientID specific escrow address ?
192 from = rlm.Address()
193 to = address(receiver)
194 amount = chain.NewCoins(coin)
195 banker = banker.NewBanker(banker.BankerTypeRealmSend, rlm)
196 )
197 banker.SendCoins(from, to, amount)
198 subEscrowForDenom(coin)
199 return nil
200}
201
202// pendingNativeEscrow is set by Transfer's native branch and read by
203// OnSendPacket. It bridges the user-call boundary check (only doable from
204// Transfer, where PreviousRealm() is the EOA) to the actual escrow update
205// (done in OnSendPacket).
206//
207// Gno persists package-level vars to the realm store at end of tx, so this
208// MUST be cleared before Transfer returns to avoid leaking into the next
209// tx. Transfer uses a defer to guarantee the reset on every exit path.
210var pendingNativeEscrow *chain.Coin