Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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)