transfer.gno
9.68 Kb · 259 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: value-moving authority must come from a direct
30 // user call because the packet sender is the tx origin.
31 if !cur.Previous().IsUserCall() {
32 panic("voucher transfer must be a direct user call (maketx call)")
33 }
34 // The voucher GRC20 token is burnt in OnSendPacket. Native coins sent
35 // with a voucher transfer would be stuck at this realm.
36 if len(unsafe.OriginSend()) > 0 {
37 panic("voucher transfer must not include native coins")
38 }
39 // Validate it exists and resolve the denom path.
40 if getVoucher(denom) == nil {
41 panic(ufmt.Errorf("voucher token not found for denom %s", denom))
42 }
43 d, found := getDenom(denom)
44 if !found {
45 panic(ufmt.Sprintf("denom %s not found in store", denom))
46 }
47 pendingVoucherSend = &pendingSend{
48 sender: unsafe.OriginCaller().String(),
49 sourceClient: clientID,
50 denom: d.Path(),
51 amount: amount,
52 }
53 defer func() { pendingVoucherSend = nil }()
54 denom = d.Path()
55 case grc20reg.Get(denom) != nil:
56 // Non-IBC GRC20 token: value-moving authority must come from a
57 // direct user call before OnSendPacket spends prior approval.
58 if !cur.Previous().IsUserCall() {
59 panic("GRC20 transfer must be a direct user call (maketx call)")
60 }
61 // Escrow via TransferFrom happens in OnSendPacket. Native coins sent
62 // with a GRC20 transfer would be stuck at this realm.
63 if len(unsafe.OriginSend()) > 0 {
64 panic("GRC20 transfer must not include native coins")
65 }
66 // Replace the denom with a slash-free alias for the IBC packet.
67 // This avoids ibc-go rejecting base denoms containing slashes.
68 alias := GRC20Alias(denom)
69 pendingGRC20Send = &pendingSend{
70 sender: unsafe.OriginCaller().String(),
71 sourceClient: clientID,
72 denom: alias,
73 amount: amount,
74 }
75 defer func() { pendingGRC20Send = nil }()
76 denom = alias
77 case strings.Contains(denom, "/"):
78 // This case exists only for a better error message: a denom with
79 // slashes that isn't in grc20reg is likely a mistyped GRC20 path.
80 // Without it, the default (native coin) branch would give a
81 // confusing "requires one non-zero coin to be sent" error.
82 panic(ufmt.Sprintf("GRC20 token %s not found in grc20reg", denom))
83 default:
84 // Native coin: only direct user-call (maketx call) is accepted, so
85 // unsafe.OriginSend() describes coins that actually landed at this
86 // realm. Intermediate code realms or `maketx run` ephemeral realms
87 // can attach -send to the tx but spend it elsewhere.
88 if !cur.Previous().IsUserCall() {
89 panic("native transfer must be a direct user call (maketx call)")
90 }
91 sent := unsafe.OriginSend()
92 if len(sent) != 1 || sent[0].Amount == 0 {
93 panic("native transfer requires one non-zero coin to be sent")
94 }
95 if sent[0].Denom != denom || sent[0].Amount != amount {
96 panic(ufmt.Sprintf(
97 "sent coin %s does not match transfer args (denom=%s, amount=%d)",
98 sent[0], denom, amount,
99 ))
100 }
101 // Hand the verified escrow off to OnSendPacket, which can no longer
102 // safely read OriginSend (its PreviousRealm is the core realm).
103 // The defer below guarantees we clear the slot in any exit path so
104 // a leftover never bleeds into a later call.
105 pendingNativeEscrow = &sent[0]
106 defer func() { pendingNativeEscrow = nil }()
107 }
108 return transfer(cur, clientID, receiver, denom, amount, timeoutTimestamp, memo)
109}
110
111// VoucherSend is a convenience helper to send an IBC voucher token from the
112// caller to the provided recipient.
113func VoucherSend(cur realm, ibcDenom string, to address, amount int64) {
114 inst := getVoucher(ibcDenom)
115 if inst == nil {
116 panic(ufmt.Errorf("voucher token not found for denom %s", ibcDenom))
117 }
118 teller := inst.ledger.ImpersonateTeller(cur.Previous().Address())
119 if err := teller.Transfer(0, cur, to, amount); err != nil {
120 panic(ufmt.Errorf("transfer voucher %s error: %v", ibcDenom, err))
121 }
122}
123
124// VoucherApprove is a convenience helper to set a spender allowance for an IBC
125// voucher token on behalf of the caller.
126func VoucherApprove(cur realm, ibcDenom string, spender address, amount int64) {
127 inst := getVoucher(ibcDenom)
128 if inst == nil {
129 panic(ufmt.Errorf("voucher token not found for denom %s", ibcDenom))
130 }
131 teller := inst.ledger.ImpersonateTeller(cur.Previous().Address())
132 if err := teller.Approve(0, cur, spender, amount); err != nil {
133 panic(ufmt.Errorf("approve voucher %s error: %v", ibcDenom, err))
134 }
135}
136
137func transfer(cur realm, clientID, receiver, denom string, amount int64, timeoutTimestamp uint64, memo string) (
138 packet types.MsgSendPacket, sequence uint64,
139) {
140 // unsafe.OriginCaller is the EOA that signed the tx; we use it as the
141 // packet Sender so Transfer can also be invoked through a wrapper realm.
142 // On the OnSendPacket side the same value is read back to verify the
143 // packet Sender matches the signing EOA.
144 data := NewFungibleTokenPacketData(
145 denom,
146 ufmt.Sprintf("%d", amount),
147 unsafe.OriginCaller().String(),
148 receiver,
149 memo,
150 )
151 packet = types.MsgSendPacket{
152 SourceClient: clientID,
153 Payloads: []types.Payload{{
154 SourcePort: PortID,
155 DestinationPort: PortID,
156 Encoding: EncodingProtobuf,
157 Value: data.ProtoMarshal(),
158 Version: V1,
159 }},
160 TimeoutTimestamp: timeoutTimestamp,
161 }
162 sequence = core.SendPacket(cross(cur), packet)
163 return
164}
165
166// refundPacketToken is a non-crossing helper; rlm is the caller's live cur,
167// needed only by unescrowNative which constructs a Banker.
168func refundPacketToken(_ int, rlm realm, sourcePort, sourceClient, sender string, token Token) error {
169 coin, err := token.ToCoin()
170 if err != nil {
171 return ufmt.Errorf("token to coin error: %v", err)
172 }
173 if token.Denom.HasPrefix(sourcePort, sourceClient) {
174 // Re-mint voucher tokens that were burnt during OnSendPacket
175 inst := getVoucher(coin.Denom)
176 if inst == nil {
177 return ufmt.Errorf("voucher token not found for denom %s", coin.Denom)
178 }
179 if err := inst.ledger.Mint(address(sender), coin.Amount); err != nil {
180 return ufmt.Errorf("re-mint voucher error: %v", err)
181 }
182 } else if isGRC20Alias(coin.Denom) {
183 // Non-IBC GRC20: return escrowed tokens to sender
184 if err := unescrowGRC20(0, rlm, sender, coin); err != nil {
185 return err
186 }
187 } else {
188 // Native: return escrowed coins to sender
189 if err := unescrowNative(0, rlm, sender, coin); err != nil {
190 return err
191 }
192 }
193 return nil
194}
195
196// unescrowGRC20 resolves the GRC20 alias and transfers the escrowed tokens
197// back to the receiver via the token's RealmTeller. Non-crossing helper; rlm
198// is needed by the grc20 teller API.
199func unescrowGRC20(_ int, rlm realm, receiver string, coin chain.Coin) error {
200 denomKey := resolveGRC20Alias(coin.Denom)
201 token := grc20reg.Get(denomKey)
202 if token == nil {
203 return ufmt.Errorf("GRC20 token %s not found in grc20reg", denomKey)
204 }
205 teller := token.RealmTeller(0, rlm)
206 if err := teller.Transfer(0, rlm, address(receiver), coin.Amount); err != nil {
207 return ufmt.Errorf("unescrow GRC20 %s error: %v", denomKey, err)
208 }
209 subEscrowForDenom(coin)
210 return nil
211}
212
213// unescrowNative sends the escrowed native coins back to the receiver via
214// the banker. Non-crossing helper; rlm is the caller's live cur (transfer
215// realm's own frame), needed to construct a Banker.
216func unescrowNative(_ int, rlm realm, receiver string, coin chain.Coin) error {
217 var (
218 // TODO no app/clientID specific escrow address ?
219 from = rlm.Address()
220 to = address(receiver)
221 amount = chain.NewCoins(coin)
222 banker = banker.NewBanker(banker.BankerTypeRealmSend, rlm)
223 )
224 banker.SendCoins(from, to, amount)
225 subEscrowForDenom(coin)
226 return nil
227}
228
229// pendingSend records the token send that Transfer authorized for OnSendPacket.
230type pendingSend struct {
231 sender string
232 sourceClient string
233 denom string
234 amount int64
235}
236
237// Gno persists package-level vars to the realm store at end of tx.
238// These pending values must be cleared before Transfer returns to avoid
239// leaking state into the next tx. Transfer uses defers to guarantee the
240// reset on every exit path.
241var (
242 // pendingNativeEscrow is set by Transfer's native branch and read by
243 // OnSendPacket. It bridges the user-call boundary check, which is only
244 // possible in Transfer where PreviousRealm() is the EOA, to the actual
245 // escrow update done in OnSendPacket.
246 pendingNativeEscrow *chain.Coin
247
248 // pendingVoucherSend is set by Transfer's voucher branch after the direct
249 // user-call guard passes. OnSendPacket consumes it before burning
250 // user-owned voucher tokens, preventing direct core.SendPacket bypasses and
251 // binding the packet to the asset the user explicitly requested.
252 pendingVoucherSend *pendingSend
253
254 // pendingGRC20Send is set by Transfer's GRC20 branch after the direct
255 // user-call guard passes. OnSendPacket consumes it before escrowing
256 // user-owned GRC20 tokens, preventing direct core.SendPacket bypasses and
257 // binding the packet to the asset the user explicitly requested.
258 pendingGRC20Send *pendingSend
259)