Search Apps Documentation Source Content File Folder Download Copy Actions Download

transfer.gno

10.14 Kb · 319 lines
  1package v1
  2
  3import (
  4	ufmt "gno.land/p/nt/ufmt/v0"
  5
  6	prabc "gno.land/p/gnoswap/rbac"
  7
  8	"gno.land/r/gnoswap/access"
  9	"gno.land/r/gnoswap/common"
 10	pl "gno.land/r/gnoswap/pool"
 11
 12	i256 "gno.land/p/gnoswap/int256"
 13	u256 "gno.land/p/gnoswap/uint256"
 14)
 15
 16// safeTransfer performs a token transfer out of the pool while ensuring
 17// the pool has sufficient balance and updating internal accounting.
 18// This function is typically used during swaps and liquidity removals.
 19//
 20// Important requirements:
 21//   - The amount must be a positive value representing tokens to transfer out
 22//   - The pool must have sufficient balance for the transfer
 23//   - The transfer amount must fit within int64 range
 24//
 25// Parameters:
 26//   - p: the pool instance
 27//   - to: destination address for the transfer
 28//   - tokenPath: path identifier of the token to transfer
 29//   - amount: amount to transfer (positive u256.Uint value)
 30//   - isToken0: true if transferring token0, false for token1
 31//
 32// The function will:
 33//  1. Check pool has sufficient balance
 34//  2. Execute the transfer
 35//  3. Subtract the amount from pool's internal balance
 36//
 37// Panics if any validation fails or if the transfer fails
 38func (i *poolV1) safeTransfer(
 39	_ int,
 40	rlm realm,
 41	p *pl.Pool,
 42	to address,
 43	tokenPath string,
 44	amount *u256.Uint,
 45	isToken0 bool,
 46) {
 47	token0 := p.BalanceToken0()
 48	token1 := p.BalanceToken1()
 49	amountInt64 := safeConvertToInt64(amount)
 50
 51	if err := validatePoolBalance(token0, token1, amountInt64, isToken0); err != nil {
 52		panic(err)
 53	}
 54
 55	common.SafeGRC20Transfer(cross(rlm), tokenPath, to, amountInt64)
 56
 57	newBalance, err := updatePoolBalance(token0, token1, amountInt64, isToken0)
 58	if err != nil {
 59		panic(err)
 60	}
 61
 62	poolBalances := p.Balances()
 63
 64	if isToken0 {
 65		poolBalances.SetToken0(newBalance)
 66	} else {
 67		poolBalances.SetToken1(newBalance)
 68	}
 69
 70	p.SetBalances(poolBalances)
 71}
 72
 73// safeTransferFrom securely transfers tokens into the pool while ensuring balance consistency.
 74//
 75// This function performs the following steps:
 76// 1. Validates and converts the transfer amount to `int64` using `safeConvertToInt64`.
 77// 2. Executes the token transfer using `TransferFrom` via the token teller contract.
 78// 3. Verifies that the destination balance reflects the correct amount after transfer.
 79// 4. Updates the pool's internal balances (`token0` or `token1`) and validates the updated state.
 80//
 81// Parameters:
 82// - p (*pl.Pool): The pool instance to transfer tokens into.
 83// - from (address): Source address for the token transfer.
 84// - to (address): Destination address, typically the pool address.
 85// - tokenPath (string): Path identifier for the token being transferred.
 86// - amount (*u256.Uint): The amount of tokens to transfer (must be a positive value).
 87// - isToken0 (bool): A flag indicating whether the token being transferred is token0 (`true`) or token1 (`false`).
 88//
 89// Panics:
 90// - If the `amount` exceeds the int64 range during conversion.
 91// - If the token transfer (`TransferFrom`) fails.
 92// - If the destination balance after the transfer does not match the expected amount.
 93// - If the pool's internal balances (`token0` or `token1`) overflow or become inconsistent.
 94//
 95// Notes:
 96// - The function assumes that the sender (`from`) has approved the pool to spend the specified tokens.
 97// - The balance consistency check ensures that no tokens are lost or double-counted during the transfer.
 98// - Pool balance updates are performed atomically to ensure internal consistency.
 99func (i *poolV1) safeTransferFrom(
100	_ int,
101	rlm realm,
102	p *pl.Pool,
103	from, to address,
104	tokenPath string,
105	amount *u256.Uint,
106	isToken0 bool,
107) {
108	amountInt64 := safeConvertToInt64(amount)
109
110	token := common.GetToken(tokenPath)
111	beforeBalance := token.BalanceOf(to)
112
113	common.SafeGRC20TransferFrom(cross(rlm), tokenPath, from, to, amountInt64)
114
115	afterBalance := token.BalanceOf(to)
116	if 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(safeAddInt64(poolBalances.Token0(), amountInt64))
128	} else {
129		poolBalances.SetToken1(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 := 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 := 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 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 safeSubInt64(token1, amount), nil
319}