An automated market maker publishes a rule that ties the balances in a pool together. Quoting a trade means solving that rule for the quantity the trader does not yet know. For some rules the solution is a formula the contract can evaluate once and be done. For others no such formula exists, and the contract has to reach the number some other way – by guessing and correcting, by approximating a function it cannot compute directly, or by refusing to price anything outside a set of values it worked out in advance.
Which of those a protocol picks is usually treated as an implementation detail, hidden below the tokenomics. It is not. The choice decides who absorbs the rounding, what the contract does when it cannot find an answer, and which parameters a DAO is really voting on when it adjusts a pool. This page reads the three production answers out of the deployed source, and then looks at what a bonding curve inherits from the same problem.
When a price has a formula, and when it does not
Uniswap v2 is the easy case. The invariant is a product of two balances, and solving it for the output amount gives a formula with one multiplication and one division. UniswapV2Library.getAmountOut is four lines long: the input is scaled by 997/1000 to take the fee, then numerator / denominator ends it. There is no search and nothing to converge on. The only inexactness is the integer division at the end, which truncates.
Curve's StableSwap is not that case. Its invariant blends a constant-sum term, which holds a peg, with a constant-product term, which guarantees liquidity at the extremes, and the blend is controlled by an amplification coefficient. Egorov's paper sets the invariant out; the deployed contract does not attempt to invert it symbolically. Both quantities a trade needs – the invariant D for the current balances, and the post-trade balance y of the outgoing asset – are found by iteration.
Balancer's weighted pools fail in a third way. Their output formula raises a balance ratio to the power of a ratio of pool weights, and the EVM has no exponentiation for non-integer exponents. An 80/20 pool needs to compute something raised to the power 4, which is easy; a 60/40 pool needs the power 1.5, which is not. The rule is perfectly well defined and simply cannot be evaluated with the arithmetic the machine offers.
Three answers to the same problem
Iterate. Curve's get_D and get_y each run a Newton–Raphson loop written as for _i in range(255). Each pass computes a better estimate and compares it to the previous one; the loop breaks the moment the two differ by 1 or less, under a source comment that reads “Equality with the precision of 1”. That unit is the smallest representable amount, so the loop is asking for agreement to the last decimal place the token has. At ordinary balances it gets there in a handful of passes. The 255 is a ceiling, not an expectation.
Approximate. Balancer does not iterate. LogExpMath.pow rewrites xy as exp(y · ln x) and builds both halves out of fixed-point series, carrying intermediate values at 20 decimals and switching to a separate 36-decimal path when the base sits within 0.1 of one, where precision matters most. The domain is bounded by the word size rather than by economics: the source derives the largest usable exponent as ln((2255 − 1) / 1020) = 130.70, then pins the constant at 130 “to have some safety margin”, with the negative bound at −41. A trade whose exponent lands outside that window reverts.
Precompute. Uniswap v3 avoids the question by shrinking the set of legal prices. Prices live on ticks of 1.0001t, and TickMath.getSqrtRatioAtTick converts a tick to a price with no loop at all: it decomposes the tick index into bits and multiplies a hard-coded constant for each set bit, nineteen constants covering the whole range from tick −887272 to 887272. Every one was computed off-chain and burned into the bytecode. The result is exact and costs a fixed amount of gas, and the price grid is discrete by construction – the v3 paper treats that granularity as the design, not a compromise.
The three sit on one trade-off. You can search for the answer and pay in iterations, approximate it and pay in error, or restrict the question and pay in expressiveness. None of them gets exactness for free.
The error has to have a direction
An approximation that is sometimes high and sometimes low is not neutral in a market, because a trader can choose when to trade and the pool cannot. Balancer's answer is to make the error one-sided on purpose. FixedPoint declares MAX_POW_RELATIVE_ERROR = 10000, a relative bound of 10−14, and then exposes two wrappers rather than one: powDown computes the raw power and subtracts the maximum possible error before returning, and powUp adds it. Whichever side of a trade the number lands on, the returned value is deliberately worse for the trader than the true power. Both wrappers also short-circuit when the exponent is exactly 1, 2 or 4, a case the source notes “occur often in 50/50 and 80/20 Weighted Pools” – those exponents are integers, so the exact path is available and the error correction is not applied at all.
Curve is blunter. In get_dy the amount paid out is computed as (xp[j] - y - 1), with a bare subtraction of one unit before the fee is even taken. There is no comment. The unit goes to the pool.
Neither of these is an accident of floating-point arithmetic, and neither is disclosed in a front end. Rounding is a policy choice about who absorbs the residue of an inexact calculation, and in both protocols the answer is the liquidity providers rather than the trader – which is the same direction, and a much smaller magnitude, as the extraction covered under MEV and value extraction.
An iteration budget is a safety property, not a gas optimisation
The obvious reading of for _i in range(255) is that it caps gas. The more useful reading is that it decides what happens when the maths does not cooperate. Curve's loop has no failure branch: if 255 passes go by without the iterate settling, the loop simply ends and the function returns the last value it held. Nothing reverts. The trade proceeds at whatever number the solver was holding when it ran out of attempts.
That is the general hazard in every iterative pricing routine. A bounded solver converts “I could not find the answer” into “here is an answer”, and the answer is a price somebody trades at. The interesting question about an iterative AMM is therefore not whether its mathematics is correct – it usually is – but what the contract does in the corner where the solver has not converged, and in which direction the unconverged estimate is wrong. An estimate that is still approaching the root from the generous side mints or pays out too much; one approaching from the other side merely short-changes the trader. Those two failures are not equally bad, and the difference is invisible unless a protocol writes the convergence direction down.
Caper's own curve carries a documented instance of exactly this. Its solver originally damped each Newton step toward a floor, which capped how fast the estimate could descend at one halving per pass. For a small payment against a mature curve the initial guess starts far above the root, and the damping meant the solver could exhaust its whole iteration budget while still above it – returning a number that would have minted too many tokens rather than reverting. The fix, recorded in the comment above the loop in contracts/logic/src/lib.rs, was to drop the damping for a hard domain floor, justified by the curve being convex on the interval so that a Newton step from any point lands at or above the root. The regression test is named for the exact cell that broke: tokens_bought_converges_for_small_payments_on_mature_curve.
Governance sets the numerical regime, not just the economics
The parameters a DAO votes on are the same ones that decide how the solve behaves, which makes a routine parameter vote a change of method as well as a change of price.
Curve's amplification coefficient is the clearest case. It cannot be set outright; ramp_A moves it over a window, requiring at least MIN_RAMP_TIME of 86,400 seconds since the last ramp began and at least that much again before the target time, and it rejects any move larger than a factor of ten in either direction, with an absolute ceiling of 106. During the window, _A() interpolates linearly on block.timestamp, so the invariant a trade is priced against is different in consecutive blocks even though nobody has traded. The rate limits exist because a discontinuous jump in the amplification would move the invariant, and therefore the pool's implied prices, in one step.
Balancer's weights work the other way round. Because the fast paths in powUp and powDown only trigger for exponents of 1, 2 and 4, a 50/50 or 80/20 pool is priced by exact integer arithmetic, and a pool with any other weighting is priced through the logarithm-and-exponential path with its bounded error and its explicit adjustment against the trader. Balancer's weighted-math documentation presents the weights as an economic knob. In the deployed code they also select which of two implementations runs. A governance vote that moves a pool from 80/20 to 75/25 has changed the pricing code path, not only the curve.
Neither protocol hides any of this – it is all in verified source. But it is not in the proposal text either. A treasury deciding pool parameters is making a numerical-methods decision whether or not anyone at the forum describes it that way.
How Caper approaches this
A caper prices on a bonding curve rather than against a pool of counterparties, but it inherits the same asymmetry, and it resolves it the same way Curve does – by iterating in the direction that has no formula.
Selling is closed form. The cumulative amount of XRD the curve has taken at a given point is an explicit expression, so a sale evaluates it before and after and returns the difference. Nothing is searched for. Buying is the inverse of that expression, which has no closed form, so the contract solves it numerically: sixteen Newton passes against a tolerance of 10−7 XRD, seeded from a power approximation and clamped into the curve's valid interval. The convergence break is deliberately one-sided – it fires only when the residual is at or just below zero, never above – so an early exit cannot leave the solver on the over-minting side of the root. The mechanics of what that means for a trader, including the buy-versus-sell asymmetry it produces in practice, are covered under Trading.
The parameters that decide the shape of the curve are fixed in the deployed logic rather than voted, so there is no equivalent of a ramp changing the invariant under a pending trade. That is a narrower design than Curve's on purpose: less to govern, and correspondingly less that a governance decision can move without anyone noticing it moved.
References
- Curve. StableSwap3Pool.vy – deployed 3pool source;
get_Dandget_yNewton loops, theget_dypayout, andramp_A/_A(). - Michael Egorov. StableSwap – efficient mechanism for Stablecoin liquidity – the invariant and the amplification coefficient.
- Balancer. LogExpMath.sol –
powas exp(y · ln x), the 20- and 36-decimal paths, and the derivation of the exponent bounds. - Balancer. FixedPoint.sol –
MAX_POW_RELATIVE_ERROR,powUp,powDown, and the integer fast paths. - Balancer. Weighted math – protocol documentation for the weighted-pool formula.
- Uniswap. UniswapV2Library.sol –
getAmountOut, the closed-form constant-product quote. - Uniswap. TickMath.sol –
getSqrtRatioAtTick, the precomputed tick constants and the tick bounds. - Hayden Adams et al. Uniswap v3 Core – ticks, the 1.0001 spacing, and concentrated-liquidity pricing.
- Caper.
contracts/logic/src/lib.rs–cumulative_xrd,compute_xrd_receivedandcompute_tokens_bought_inner, read directly for the Caper claims on this page.