Search Apps Documentation Source Content File Folder Download Copy Actions Download

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