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

transfer.gno

10.25 Kb · 319 lines
  1package pool
  2
  3import (
  4	ufmt "gno.land/p/nt/ufmt/v0"
  5
  6	prabc "gno.land/p/gnoswap/rbac/v1"
  7
  8	"gno.land/r/gnoswap/access/v1"
  9	"gno.land/r/gnoswap/common"
 10	pl "gno.land/r/gnoswap/pool"
 11
 12	"gno.land/p/gnoswap/gnsmath/v1"
 13	i256 "gno.land/p/gnoswap/int256/v1"
 14	u256 "gno.land/p/gnoswap/uint256/v1"
 15)
 16
 17// safeTransfer performs a token transfer out of the pool while ensuring
 18// the pool has sufficient balance and updating internal accounting.
 19// This function is typically used during swaps and liquidity removals.
 20//
 21// Important requirements:
 22//   - The amount must be a positive value representing tokens to transfer out
 23//   - The pool must have sufficient balance for the transfer
 24//   - The transfer amount must fit within int64 range
 25//
 26// Parameters:
 27//   - p: the pool instance
 28//   - to: destination address for the transfer
 29//   - tokenPath: path identifier of the token to transfer
 30//   - amount: amount to transfer (positive u256.Uint value)
 31//   - isToken0: true if transferring token0, false for token1
 32//
 33// The function will:
 34//  1. Check pool has sufficient balance
 35//  2. Execute the transfer
 36//  3. Subtract the amount from pool's internal balance
 37//
 38// Panics if any validation fails or if the transfer fails
 39func (i *poolV1) safeTransfer(
 40	_ int,
 41	rlm realm,
 42	p *pl.Pool,
 43	to address,
 44	tokenPath string,
 45	amount *u256.Uint,
 46	isToken0 bool,
 47) {
 48	token0 := p.BalanceToken0()
 49	token1 := p.BalanceToken1()
 50	amountInt64 := gnsmath.SafeConvertToInt64(amount)
 51
 52	if err := validatePoolBalance(token0, token1, amountInt64, isToken0); err != nil {
 53		panic(err)
 54	}
 55
 56	common.SafeGRC20Transfer(0, rlm, tokenPath, to, amountInt64)
 57
 58	newBalance, err := updatePoolBalance(token0, token1, amountInt64, isToken0)
 59	if err != nil {
 60		panic(err)
 61	}
 62
 63	poolBalances := p.Balances()
 64
 65	if isToken0 {
 66		poolBalances.SetToken0(newBalance)
 67	} else {
 68		poolBalances.SetToken1(newBalance)
 69	}
 70
 71	p.SetBalances(poolBalances)
 72}
 73
 74// safeTransferFrom securely transfers tokens into the pool while ensuring balance consistency.
 75//
 76// This function performs the following steps:
 77// 1. Validates and converts the transfer amount to `int64` using `gnsmath.SafeConvertToInt64`.
 78// 2. Executes the token transfer using `TransferFrom` via the token teller contract.
 79// 3. Verifies that the destination balance reflects the correct amount after transfer.
 80// 4. Updates the pool's internal balances (`token0` or `token1`) and validates the updated state.
 81//
 82// Parameters:
 83// - p (*pl.Pool): The pool instance to transfer tokens into.
 84// - from (address): Source address for the token transfer.
 85// - to (address): Destination address, typically the pool address.
 86// - tokenPath (string): Path identifier for the token being transferred.
 87// - amount (*u256.Uint): The amount of tokens to transfer (must be a positive value).
 88// - isToken0 (bool): A flag indicating whether the token being transferred is token0 (`true`) or token1 (`false`).
 89//
 90// Panics:
 91// - If the `amount` exceeds the int64 range during conversion.
 92// - If the token transfer (`TransferFrom`) fails.
 93// - If the destination balance after the transfer does not match the expected amount.
 94// - If the pool's internal balances (`token0` or `token1`) overflow or become inconsistent.
 95//
 96// Notes:
 97// - The function assumes that the sender (`from`) has approved the pool to spend the specified tokens.
 98// - The balance consistency check ensures that no tokens are lost or double-counted during the transfer.
 99// - Pool balance updates are performed atomically to ensure internal consistency.
100func (i *poolV1) safeTransferFrom(
101	_ int,
102	rlm realm,
103	p *pl.Pool,
104	from, to address,
105	tokenPath string,
106	amount *u256.Uint,
107	isToken0 bool,
108) {
109	amountInt64 := gnsmath.SafeConvertToInt64(amount)
110
111	beforeBalance := common.BalanceOf(tokenPath, to)
112
113	common.SafeGRC20TransferFrom(0, rlm, tokenPath, from, to, amountInt64)
114
115	afterBalance := common.BalanceOf(tokenPath, to)
116	if gnsmath.SafeAddInt64(beforeBalance, amountInt64) != afterBalance {
117		panic(ufmt.Sprintf(
118			"%v. beforeBalance(%d) + amount(%d) != afterBalance(%d)",
119			errTransferFailed, beforeBalance, amountInt64, afterBalance,
120		))
121	}
122
123	poolBalances := p.Balances()
124
125	// update pool balances
126	if isToken0 {
127		poolBalances.SetToken0(gnsmath.SafeAddInt64(poolBalances.Token0(), amountInt64))
128	} else {
129		poolBalances.SetToken1(gnsmath.SafeAddInt64(poolBalances.Token1(), amountInt64))
130	}
131
132	p.SetBalances(poolBalances)
133}
134
135// safeSwapCallback executes a swap callback and validates the token payment.
136//
137// This function implements the flash swap pattern where the pool first sends output tokens
138// to the recipient, then invokes the callback to receive input tokens from the caller.
139// The callback is responsible for transferring the required input tokens to the pool.
140//
141// Callback Signature:
142//
143//	func(cur realm, amount0Delta, amount1Delta int64, _ *pool.CallbackMarker) error
144//
145// Delta Convention (following Uniswap V3 standard):
146//   - Positive delta: Amount the pool must RECEIVE (input token)
147//   - Negative delta: Amount the pool has SENT (output token)
148//
149// For zeroForOne swaps (token0 → token1):
150//   - amount0Delta > 0: Pool receives token0 (input)
151//   - amount1Delta < 0: Pool sends token1 (output)
152//
153// For oneForZero swaps (token1 → token0):
154//   - amount0Delta < 0: Pool sends token0 (output)
155//   - amount1Delta > 0: Pool receives token1 (input)
156//
157// Parameters:
158//   - p: The pool instance
159//   - tokenPath: Path of the input token that pool must receive
160//   - amountInInt256: Amount pool must receive (positive i256.Int)
161//   - amountOutInt256: Amount pool has sent (negative i256.Int)
162//   - zeroForOne: Swap direction (true = token0 to token1)
163//   - swapCallback: Callback function that must transfer input tokens to pool
164//
165// The callback needed:
166//  1. Verify the caller is the legitimate pool contract address to prevent unauthorized invocations
167//  2. Transfer at least `amountIn` of input tokens to the pool
168//  3. Return nil on success, or an error to revert the swap
169//
170// Example callback implementation:
171//
172//	func(cur realm, amount0Delta, amount1Delta int64, _ *pool.CallbackMarker) error {
173//	    // Security check: ensure this callback is invoked by the legitimate pool
174//	    caller := runtime.PreviousRealm().Address()
175//	    poolAddr := chain.PackageAddress("gno.land/r/gnoswap/pool")
176//
177//	    if caller != poolAddr {
178//	        panic("unauthorized caller")
179//	    }
180//
181//	    if amount0Delta > 0 {
182//	        // Transfer token0 to pool
183//	        token0.Transfer(cross, poolAddr, amount0Delta)
184//	    }
185//	    if amount1Delta > 0 {
186//	        // Transfer token1 to pool
187//	        token1.Transfer(cross, poolAddr, amount1Delta)
188//	    }
189//	    return nil
190//	}
191func (i *poolV1) safeSwapCallback(
192	_ int,
193	rlm realm,
194	p *pl.Pool,
195	tokenPath string,
196	amountInInt256 *i256.Int,
197	amountOutInt256 *i256.Int,
198	zeroForOne bool,
199	swapCallback func(cur realm, amount0Delta, amount1Delta int64, _ *pl.CallbackMarker) error,
200) {
201	if swapCallback == nil {
202		panic(makeErrorWithDetails(
203			errInvalidInput,
204			"swapCallback is nil",
205		))
206	}
207
208	currentAddress := access.MustGetAddress(prabc.ROLE_POOL.String())
209
210	amountIn := amountInInt256.Int64()
211	amountOut := amountOutInt256.Int64()
212	balanceBefore := common.BalanceOf(tokenPath, currentAddress)
213
214	// Make callback to the calling contract
215	// The contract should transfer tokens to pool within this callback
216	// Following Uniswap V3 convention:
217	// - Positive delta: amount the pool must receive (input token)
218	// - Negative delta: amount the pool has sent (output token)
219	var amount0Delta, amount1Delta, beforeTokenBalance int64
220
221	if zeroForOne {
222		// zeroForOne: pool receives token0 (positive), sends token1 (negative)
223		amount0Delta = amountIn
224		amount1Delta = amountOut
225		beforeTokenBalance = p.BalanceToken0()
226	} else {
227		// !zeroForOne: pool receives token1 (positive), sends token0 (negative)
228		amount0Delta = amountOut
229		amount1Delta = amountIn
230		beforeTokenBalance = p.BalanceToken1()
231	}
232
233	// execute swap callback
234	// CallbackMarker is created by the pool module to enforce type-system level validation.
235	// Allocation goes through pl.NewCallbackMarker so the alloc happens in pool realm
236	// (interrealm v2 forbids /r/-declared types being constructed outside their owner).
237	err := swapCallback(cross(rlm), amount0Delta, amount1Delta, pl.NewCallbackMarker())
238	if err != nil {
239		panic(err)
240	}
241
242	balanceAfter := common.BalanceOf(tokenPath, currentAddress)
243	balanceIncrease := gnsmath.SafeSubInt64(balanceAfter, balanceBefore)
244
245	// check insufficient payment by swap callback
246	if balanceIncrease < amountIn {
247		panic(makeErrorWithDetails(
248			errInsufficientPayment,
249			ufmt.Sprintf("insufficient payment: expected %d, received %d", amountIn, balanceIncrease),
250		))
251	}
252
253	// check overflow update pool balance
254	resultTokenBalance := gnsmath.SafeAddInt64(beforeTokenBalance, amountIn)
255
256	poolBalances := p.Balances()
257
258	if zeroForOne {
259		poolBalances.SetToken0(resultTokenBalance)
260	} else {
261		poolBalances.SetToken1(resultTokenBalance)
262	}
263
264	p.SetBalances(poolBalances)
265}
266
267// validatePoolBalance checks if the pool has sufficient balance of either token0 and token1
268// before proceeding with a transfer. This prevents the pool won't go into a negative balance.
269func validatePoolBalance(token0, token1, amount int64, isToken0 bool) error {
270	if amount < 0 {
271		return ufmt.Errorf("%v. amount(%d) must be positive", errTransferFailed, amount)
272	}
273
274	if isToken0 {
275		if token0 < amount {
276			return ufmt.Errorf(
277				"%v. token0(%d) >= amount(%d)",
278				errTransferFailed, token0, amount,
279			)
280		}
281		return nil
282	}
283	if token1 < amount {
284		return ufmt.Errorf(
285			"%v. token1(%d) >= amount(%d)",
286			errTransferFailed, token1, amount,
287		)
288	}
289	return nil
290}
291
292// updatePoolBalance calculates the new balance after a transfer and validate.
293// It ensures the resulting balance won't be negative or overflow.
294func updatePoolBalance(
295	token0, token1, amount int64,
296	isToken0 bool,
297) (int64, error) {
298	if amount < 0 {
299		return 0, ufmt.Errorf("%v. amount(%d) must be positive", errBalanceUpdateFailed, amount)
300	}
301
302	if isToken0 {
303		if token0 < amount {
304			return 0, ufmt.Errorf(
305				"%v. cannot decrease, token0(%d) - amount(%d)",
306				errBalanceUpdateFailed, token0, amount,
307			)
308		}
309		return gnsmath.SafeSubInt64(token0, amount), nil
310	}
311
312	if token1 < amount {
313		return 0, ufmt.Errorf(
314			"%v. cannot decrease, token1(%d) - amount(%d)",
315			errBalanceUpdateFailed, token1, amount,
316		)
317	}
318	return gnsmath.SafeSubInt64(token1, amount), nil
319}