Appearance
Making a Swap with POE
| Error | Cause |
|---|---|
Callback__InvalidCallback | Callback didn't return the swapCallback function selector 0xfa483e72 |
Callback__ZeroTokenReceived | Callback didn't transfer any tokens to the pool |
Callback__InsufficientTokenReceived | Callback transferred less than required |
OraclePool__OracleNotSet | Pool's oracle is not configured |
OracleData__Expired | Oracle data has expired, operator needs to update |
This guide explains how to integrate with OraclePool to execute token swaps. The pool uses a callback pattern similar to Uniswap V3 - you call swap(), receive tokens optimistically, then must pay via a callback.
Getting a Quote
Before executing a swap, you can preview the expected output using getQuote():
solidity
function getQuote(bool swapXtoY, uint256 amountIn)
external view
returns (uint256 amountOut, uint256 actualAmountIn, uint256 feeIn, uint256 feeOut);Parameters:
swapXtoY- Direction of the swap.true= sell tokenX for tokenY,false= sell tokenY for tokenXamountIn- The amount of input token you want to swap (fee-inclusive)
Returns:
amountOut- Amount of output token the recipient will receive (afterfeeOut, if any)actualAmountIn- Gross input amount the pool will consume (fee-inclusive). May be less thanamountInif the curve would otherwise drain the pool.feeIn- Fee charged in the input token. Non-zero only for Y → X swaps.feeOut- Fee charged in the output token. Non-zero only for X → Y swaps.
Exactly one of feeIn / feeOut is non-zero. Both fees are denominated in tokenY (the pool's quote token), since X → Y fees come out of the output side and Y → X fees come out of the input side.
Example:
solidity
// Check how much tokenY we'd get for 1000 tokenX
(uint256 amountOut, uint256 actualAmountIn, uint256 feeIn, uint256 feeOut) =
pool.getQuote(true, 1000e18);Reading Pool Info
To understand what tokens are involved:
solidity
(address tokenX, address tokenY) = pool.getTokens();
(uint256 balanceX, uint256 balanceY) = pool.getBalances();Executing a Swap
The swap function uses an optimistic transfer pattern with a callback:
solidity
function swap(
address recipient,
bool swapXtoY,
uint256 amountIn,
bytes calldata data
) external returns (int256 deltaX, int256 deltaY);Parameters:
recipient- Address that receives the output tokensswapXtoY- Direction:true= tokenX → tokenY,false= tokenY → tokenXamountIn- Amount of input token to swapdata- Arbitrary data passed through to the callback
Returns:
deltaX- Change in tokenX from pool's perspective (positive = pool received, negative = pool sent)deltaY- Change in tokenY from pool's perspective
Swap Flow
Call swap()
Your contract calls pool.swap(recipient, swapXtoY, amountIn, data).
Pool transfers output
Pool calculates amountOut and transfers tokenOut to recipient.
Pool calls back
Pool calls swapCallback(deltaX, deltaY, data) on msg.sender (your contract).
Your callback pays
Your callback must push tokenIn to the pool (msg.sender inside the callback) via transfer / safeTransfer. The pool checks its own balanceOf delta — it never calls transferFrom on your behalf.
Callback returns selector
Your callback must return 0xfa483e72 (the function selector).
Pool verifies payment
Pool verifies it received enough tokens (≥ actualAmountIn). Overpayment is kept by the pool as a donation to LPs.
swap() returns
swap() returns with the final deltas (from the pool's perspective).
Implementing the Callback
Your contract must implement the ISwapCallback interface:
solidity
interface ISwapCallback {
function swapCallback(int256 deltaX, int256 deltaY, bytes calldata data)
external
returns (bytes4);
}The callback must:
Transfer the required input tokens to the pool (msg.sender in the callback).
Return the selector 0xfa483e72 (which is ISwapCallback.swapCallback.selector).
Important: The callback is called with deltaX and deltaY from the pool's perspective:
- Positive delta = pool expects to receive that token
- Negative delta = pool has sent that token
Solidity Example: Router Contract
Here's a complete example of a router contract that can swap through OraclePool:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface IOraclePool {
function getTokens() external view returns (address tokenX, address tokenY);
function getQuote(bool swapXtoY, uint256 amountIn)
external view
returns (uint256 amountOut, uint256 actualAmountIn, uint256 feeIn, uint256 feeOut);
function swap(address recipient, bool swapXtoY, uint256 amountIn, bytes calldata data)
external returns (int256 deltaX, int256 deltaY);
}
interface ISwapCallback {
function swapCallback(int256 deltaX, int256 deltaY, bytes calldata data)
external returns (bytes4);
}
contract OraclePoolRouter is ISwapCallback {
using SafeERC20 for IERC20;
// Track expected callback to prevent unauthorized calls
address private _expectedPool;
/// @notice Swap exact input tokens for output tokens
/// @param pool The OraclePool to swap through
/// @param swapXtoY Direction: true = tokenX -> tokenY
/// @param amountIn Amount of input token to swap
/// @param minAmountOut Minimum output amount (slippage protection)
/// @param recipient Address to receive output tokens
/// @return amountOut Actual amount of output tokens received
function swapExactInput(
address pool,
bool swapXtoY,
uint256 amountIn,
uint256 minAmountOut,
address recipient
) external returns (uint256 amountOut) {
// Get the input token
(address tokenX, address tokenY) = IOraclePool(pool).getTokens();
address tokenIn = swapXtoY ? tokenX : tokenY;
// Transfer input tokens from user to this contract
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
// Set expected pool for callback verification
_expectedPool = pool;
// Execute the swap - pool will call our swapCallback
(int256 deltaX, int256 deltaY) = IOraclePool(pool).swap(
recipient,
swapXtoY,
amountIn,
abi.encode(tokenIn) // Pass tokenIn address in callback data
);
// Clear expected pool
_expectedPool = address(0);
// Calculate actual output (negative delta = pool sent tokens)
int256 outDelta = swapXtoY ? deltaY : deltaX;
amountOut = uint256(-outDelta);
// Slippage check
require(amountOut >= minAmountOut, "Insufficient output");
}
/// @notice Callback called by the pool during swap
/// @dev Must transfer input tokens to the pool and return correct selector
function swapCallback(
int256 deltaX,
int256 deltaY,
bytes calldata data
) external override returns (bytes4) {
// Verify caller is the expected pool
require(msg.sender == _expectedPool, "Unauthorized callback");
// Decode the token we need to pay
address tokenIn = abi.decode(data, (address));
// Determine amount to pay (positive delta = pool expects payment)
uint256 amountToPay = deltaX > 0 ? uint256(deltaX) : uint256(deltaY);
// Transfer tokens to the pool
IERC20(tokenIn).safeTransfer(msg.sender, amountToPay);
// Return the required selector
return ISwapCallback.swapCallback.selector; // 0xfa483e72
}
}Usage Example
solidity
// User wants to swap 100 tokenX for tokenY with 1% slippage tolerance
OraclePoolRouter router = OraclePoolRouter(routerAddress);
IOraclePool pool = IOraclePool(poolAddress);
// First, get a quote
(uint256 expectedOut, , , ) = pool.getQuote(true, 100e18);
uint256 minOut = expectedOut * 99 / 100; // 1% slippage
// Approve router to spend tokens
(address tokenX, ) = pool.getTokens();
IERC20(tokenX).approve(address(router), 100e18);
// Execute swap
uint256 received = router.swapExactInput(
address(pool),
true, // swapXtoY
100e18, // amountIn
minOut, // minAmountOut
msg.sender // recipient
);TypeScript Example: Reading Quotes
typescript
import { ethers } from 'ethers';
const ORACLE_POOL_ABI = [
"function getTokens() view returns (address tokenX, address tokenY)",
"function getBalances() view returns (uint256 totalX, uint256 totalY)",
"function getQuote(bool swapXtoY, uint256 amountIn) view returns (uint256 amountOut, uint256 actualAmountIn, uint256 feeIn, uint256 feeOut)"
];
async function getSwapQuote(
provider: ethers.Provider,
poolAddress: string,
swapXtoY: boolean,
amountIn: bigint
) {
const pool = new ethers.Contract(poolAddress, ORACLE_POOL_ABI, provider);
// Get pool info
const [tokens, balances] = await Promise.all([
pool.getTokens(),
pool.getBalances()
]);
console.log(`Pool: ${tokens.tokenX} / ${tokens.tokenY}`);
console.log(`Balances: ${balances.totalX} / ${balances.totalY}`);
// Get quote
const [amountOut, actualAmountIn, feeIn, feeOut] =
await pool.getQuote(swapXtoY, amountIn);
console.log(`Input (gross): ${actualAmountIn}`);
console.log(`Output: ${amountOut}`);
console.log(`Fee in / Fee out: ${feeIn} / ${feeOut}`);
return { amountOut, actualAmountIn, feeIn, feeOut };
}
// Example usage
const provider = new ethers.JsonRpcProvider("https://rpc.example.com");
await getSwapQuote(
provider,
"0x...", // pool address
true, // swap tokenX to tokenY
ethers.parseEther("100") // 100 tokens
);Error Handling
Selected reverts you may encounter on the swap path:
| Error | Source | Cause |
|---|---|---|
Callback__InvalidCallback() | Callback library | Callback didn't return the selector 0xfa483e72, or the call reverted. |
Callback__ZeroTokenReceived(address token) | Callback library | Pool's balance of token did not increase during the callback. |
Callback__InsufficientTokenReceived(address, uint256, uint256) | Callback library | Pool received some token but less than amountIn. |
Callback__Overflow() | Callback library | amountIn or amountOut ≥ 2^255 (would overflow signed delta). |
OraclePool__OracleNotSet() | OraclePool | Pool's oracle is address(0). |
OraclePool__ZeroAmount() | OraclePool | Quote produced amountOut == 0. |
OracleData__Expired() | OracleData library | Oracle data for this pair has expired; operator must call setData again. |
OracleData__InvalidPrice() | OracleData library | Oracle has no data (or price == 0) for this pair. |
Security Considerations
- Callback Verification: Always verify that the callback caller is the expected pool to prevent malicious contracts from draining your tokens.
- Slippage Protection: Always use
minAmountOutchecks to protect against price movement between quote and execution. - Reentrancy: The callback pattern can be vulnerable to reentrancy. Use reentrancy guards if your callback does anything complex.
- Token Approvals: Be careful with token approvals. Only approve what's needed for the specific swap.
Agent Instructions: Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.
Perform an HTTP GET request on the current page URL with the ask query parameter:
GET /poe/making-a-swap-with-poe.md?ask=<question>The question should be specific, self-contained, and written in natural language. The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.
Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
