How are prices determined?

As we learned in the Protocol Overview, each pair on Shibaswap v1 is actually underpinned by a liquidity pool. Liquidity pools are smart contracts that hold balances of two unique tokens and enforce rules around depositing and withdrawing them. The primary rule is the constant product formula. When a token is withdrawn (bought), a proportional amount must be deposited (sold) to maintain the constant. The ratio of tokens in the pool, in combination with the constant product formula, ultimately determine the price that a swap executes at.

How Shibaswap handles prices

In Shibaswap V1, trades are always executed at the “best possible” price, calculated at execution time. Somewhat confusingly, this calculation is actually accomplished with one of two different formulas, depending on whether the trade specifies an exact input or output amount. Functionally, the difference between these two functions is minuscule, but the very existence of a difference increases conceptual complexity. Initial attempts to support both functions in V2 proved inelegant, and the decision was made to not provide any pricing functions in the core. Instead, pairs directly check whether the invariant was satisfied (accounting for fees) after every trade. This means that rather than relying on a pricing function to also enforce the invariant, V2 pairs simply and transparently ensure their own safety, a nice separation of concerns. One downstream benefit is that V2 pairs will more naturally support other flavors of trades which may emerge, (e.g. trading to a specific price at execution time). At a high level, in Shibaswap V1, trades must be priced in the periphery. The good news is that the library provides a variety of functions designed to make this quite simple, and all swapping functions in the router are designed with this in mind.

Pricing Trades

When swapping tokens on Shibaswap, it’s common to want to receive as many output tokens as possible for an exact input amount, or to pay as few input tokens as possible for an exact output amount. In order to calculate these amounts, a contract must look up the current reserves of a pair, in order to understand what the current price is. However, it is not safe to perform this lookup and rely on the results without access to an external price. Say a smart contract naively wants to send 10 DAI to the DAI/WETH pair and receive as much WETH as it can get, given the current reserve ratio. If, when called, the naive smart contract simply looks up the current price and executes the trade, it is vulnerable to front-running and will likely suffer an economic loss. To see why, consider a malicious actor who sees this transaction before it is confirmed. They could execute a swap which dramatically changes the DAI/WETH price immediately before the naive swap goes through, wait for the naive swap to execute at a bad rate, and then swap to change the price back to what it was before the naive swap. This attack is fairly cheap and low-risk, and can typically be performed for a profit. To prevent these types of attacks, it’s vital to submit swaps that have access to knowledge about the “fair” price their swap should execute at. In other words, swaps need access to an oracle, to be sure that the best execution they can get from Shibaswap is close enough to what the oracle considers the “true” price. While this may sound complicated, the oracle can be as simple as an off-chain observation of the current market price of a pair. Because of arbitrage, it’s typically the case that the ratio of the intra-block reserves of a pair is close to the “true” market price. So, if a user submits a trade with this knowledge in mind, they can ensure that the losses due to front-running are tightly bounded. This is how, for example, the Shibaswap frontend ensures trade safety. It calculates the optimal input/output amounts given observed intra-block prices, and uses the router to perform the swap, which guarantees the swap will execute at a rate no less that x% worse than the observed intra-block rate, where x is a user-specified slippage tolerance (0.5% by default). There are, of course, other options for oracles, including native V2 oracles.
Always use slippage protection when executing swaps to prevent front-running attacks and ensure you get a fair price for your trades.

Exact Input

If you’d like to send an exact amount of input tokens in exchange for as many output tokens as possible, you’ll want to use getAmountsOut. The equivalent SDK function is getOutputAmount, or minimumAmountOut for slippage calculations.

Router02 Functions for Exact Input

// Swap exact tokens for tokens
function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external returns (uint[] memory amounts);

// Swap exact ETH for tokens
function swapExactETHForTokens(
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external payable returns (uint[] memory amounts);

// Swap exact tokens for ETH
function swapExactTokensForETH(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external returns (uint[] memory amounts);

Example: Exact Input Swap

// Swap exactly 1 WETH for as much DAI as possible
const amountIn = '1000000000000000000' // 1 WETH
const path = [WETH.address, DAI.address]
const amountOutMin = '1800' // Minimum 1800 DAI (with slippage protection)

await router.swapExactTokensForTokens(
  amountIn,
  amountOutMin,
  path,
  userAddress,
  deadline
)

Exact Output

If you’d like to receive an exact amount of output tokens for as few input tokens as possible, you’ll want to use getAmountsIn. The equivalent SDK function is getInputAmount, or maximumAmountIn for slippage calculations.

Router02 Functions for Exact Output

// Swap tokens for exact tokens
function swapTokensForExactTokens(
    uint amountOut,
    uint amountInMax,
    address[] calldata path,
    address to,
    uint deadline
) external returns (uint[] memory amounts);

// Swap ETH for exact tokens
function swapETHForExactTokens(
    uint amountOut,
    address[] calldata path,
    address to,
    uint deadline
) external payable returns (uint[] memory amounts);

// Swap tokens for exact ETH
function swapTokensForExactETH(
    uint amountOut,
    uint amountInMax,
    address[] calldata path,
    address to,
    uint deadline
) external returns (uint[] memory amounts);

Example: Exact Output Swap

// Swap tokens to receive exactly 2000 DAI
const amountOut = '2000000000000000000000' // 2000 DAI
const path = [WETH.address, DAI.address]
const amountInMax = '1200000000000000000' // Maximum 1.2 WETH (with slippage protection)

await router.swapTokensForExactTokens(
  amountOut,
  amountInMax,
  path,
  userAddress,
  deadline
)

Price Calculation Functions

The Library contract provides several functions for calculating prices and amounts:
// Calculate output amount for exact input
function getAmountOut(
    uint amountIn,
    uint reserveIn,
    uint reserveOut
) internal pure returns (uint amountOut);

// Calculate input amount for exact output
function getAmountIn(
    uint amountOut,
    uint reserveIn,
    uint reserveOut
) internal pure returns (uint amountIn);

// Calculate amounts for multi-hop swaps (exact input)
function getAmountsOut(
    uint amountIn,
    address[] memory path
) internal view returns (uint[] memory amounts);

// Calculate amounts for multi-hop swaps (exact output)
function getAmountsIn(
    address factory,
    uint amountOut,
    address[] memory path
) internal view returns (uint[] memory amounts);

Slippage Protection

Slippage protection is crucial for preventing front-running attacks and ensuring fair pricing:
// Calculate minimum output with slippage
uint256 slippageTolerance = 50; // 0.5%
uint256 amountOutMin = amountOut * (10000 - slippageTolerance) / 10000;

// Calculate maximum input with slippage
uint256 amountInMax = amountIn * (10000 + slippageTolerance) / 10000;
Set slippage tolerance based on market conditions. Higher volatility requires higher slippage tolerance, but this also increases the risk of getting a worse price.

Price Impact

Price impact measures how much your trade affects the market price:
// Get price impact from trade
const priceImpact = trade.priceImpact.toSignificant(2)
console.log(`Price impact: ${priceImpact}%`)

// Check if price impact is acceptable
if (trade.priceImpact.greaterThan(new Percent('5', '100'))) {
  console.log('Warning: High price impact detected')
}
High price impact trades can result in significant slippage and poor execution prices. Consider breaking large trades into smaller ones or using different routes.

Multi-Hop Routing

For complex swaps involving multiple pairs, use multi-hop routing:
// Route: WETH → USDC → DAI
const wethUsdcPair = await createPair(WETH, USDC)
const usdcDaiPair = await createPair(USDC, DAI)

const route = new Route([wethUsdcPair, usdcDaiPair], WETH, DAI)
const trade = new Trade(route, amount, TradeType.EXACT_INPUT)

// Get amounts for each hop
const amounts = trade.swaps.map(swap => swap.outputAmount.toExact())
console.log(`WETH → USDC: ${amounts[0]}`)
console.log(`USDC → DAI: ${amounts[1]}`)
Multi-hop routing can sometimes provide better prices than direct swaps, especially when there’s more liquidity in intermediate pairs.

Best Practices

For Traders

  1. Always use slippage protection with appropriate tolerance
  2. Check price impact before executing large trades
  3. Use multi-hop routing when it provides better prices
  4. Set reasonable deadlines to avoid transaction failures
  5. Monitor gas prices to optimize transaction costs

For Developers

  1. Use the SDK for accurate price calculations
  2. Implement proper error handling for failed swaps
  3. Cache reserve data when possible to reduce RPC calls
  4. Validate token addresses before creating routes
  5. Test with small amounts before deploying to production
Never execute swaps without slippage protection. This makes your transactions vulnerable to front-running attacks and can result in significant losses.