Pegged swap

PeggedSwap is the pegged strategy archetype in 1inch Aqua, one of three archetypes (xyc, concentrated, pegged) served by a single router through SwapVM, the Aqua swap engine. It targets correlated or pegged assets such as stablecoins, wrapped tokens, and liquid staking tokens. The archetype applies a square-root linear curve that holds near-zero slippage within a narrow price band and degrades gracefully outside it. The design intent is analogous to Curve StableSwap; the internal math and fee integration differ.

One router (AquaSwapVMRouter) serves all three strategy archetypes. The archetype is selected by the program bytes shipped with the strategy, not by a separate contract address. Maker tokens stay in the maker's wallet under a revocable, per-chain, per-token allowance and move only when a taker fills a swap atomically; Aqua holds no tokens itself.


Core instructions

Instruction Role
_dynamicBalancesXD Load reserves; persist after swap
_peggedSwapGrowPriceRange2D Square-root linear pricing with peg parameters

_peggedSwapGrowPriceRange2D is a terminal instruction. It computes both the reserve transformation and the swap amounts in one step.


Program

Solidity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Program memory program = ProgramBuilder.init(_opcodes());
bytes memory bytecode = bytes.concat(
    program.build(_dynamicBalancesXD, BalancesArgsBuilder.build(
        dynamic([tokenLt, tokenGt]),
        dynamic([uint256(1_000e18), uint256(1_000e18)])
    )),
    program.build(_peggedSwapGrowPriceRange2D, PeggedSwapArgsBuilder.build(
        PeggedSwapArgsBuilder.Args({
            x0: x0,
            y0: y0,
            linearWidth: linearWidth,
            rateLt: rateLt,
            rateGt: rateGt
        })
    ))
);

Token ordering: same convention as concentrated liquidity. tokenLt has the lower address and tokenGt the higher. Price P = tokenGt / tokenLt.


Args (from PeggedSwap)

Field Size Type Description
x0 32 bytes uint256 Reserve offset for tokenLt (peg anchor)
y0 32 bytes uint256 Reserve offset for tokenGt (peg anchor)
linearWidth 32 bytes uint256 Linear-term coefficient A, scaled by 1e27 (ONE)
rateLt 32 bytes uint256 Rate numerator for tokenLt (peg ratio)
rateGt 32 bytes uint256 Rate denominator for tokenGt (peg ratio)

Total: 160 bytes, packed via abi.encodePacked.


Peg ratio

Set rateLt and rateGt to express the target exchange rate. For a 1:1 peg (e.g. USDC/USDT): rateLt = 1, rateGt = 1. For a 1:1.001 peg: rateLt = 1000, rateGt = 1001.

linearWidth controls how wide the near-zero-slippage band is before the curve steepens.


Optional modifiers

Goal Add
Flat fee _flatFeeAmountInXD before _peggedSwapGrowPriceRange2D
Expiry _deadline before balance setup

Pegged curve (verified from source)

The claims below are confirmed against 1inch/swap-vm at src/instructions/PeggedSwap.sol and src/libs/PeggedSwapMath.sol. One documented value is corrected.

Function signature (verified)

Solidity
1
function _peggedSwapGrowPriceRange2D(Context memory ctx, bytes calldata args) internal pure

The name _peggedSwapGrowPriceRange2D, the Context memory ctx, bytes calldata args parameters, and the internal pure mutability all match source exactly.

Args layout (verified)

Packed via abi.encodePacked in PeggedSwapArgsBuilder.build; parsed by zero-copy cast in PeggedSwapArgsBuilder.parse, which requires data.length >= 160 (5 * 32 bytes).

Name Type Size Offset Meaning (from source)
x0 uint256 32 bytes 0 Initial X reserve / normalization factor (token with LOWER address)
y0 uint256 32 bytes 32 Initial Y reserve / normalization factor (token with GREATER address)
linearWidth uint256 32 bytes 64 Linear-term coefficient A, scaled by 1e27 (ONE); must be <= MAX_LINEAR_WIDTH
rateLt uint256 32 bytes 96 Decimal-scale multiplier for token with LOWER address
rateGt uint256 32 bytes 128 Decimal-scale multiplier for token with GREATER address

Total: 160 bytes. Confirmed. Scale: 1e27. Confirmed — PeggedSwapMath.ONE = 1e27, chosen for precision (source: "Uses 1e27 scale ... reduces rounding error by ~10^9").

Curve family (verified)

Square-root linear — confirmed. Contract title: "Square-root linear swap curve for pegged assets". Invariant, with u = x/X₀ and v = y/Y₀ both scaled by ONE:

√(x/X₀) + √(y/Y₀) + A(x/X₀ + y/Y₀) = 1 + A

Curvature p = 0.5 is hardcoded (enables the closed-form quadratic solve in PeggedSwapMath.solve: aw² + w = c − √u − au, solved with the numerically stable root w = 2R / (1 + √D)). No iterative solver is used.

One-line behavior: the linear term A(u+v) flattens the curve near the 1:1 peg (higher A → tighter peg, lower slippage), while the square-root terms dominate at the extremes to provide smooth price protection.

Correction — max linearWidth is 5000×ONE, not 2×ONE. The parameter table and error table state a maximum of 2 × 1e27 and an error condition of linearWidth > 2e27. Source defines MAX_LINEAR_WIDTH = 5000 * ONE = 5000e27 (PeggedSwapMath.sol). The PeggedSwapInvalidLinearWidth error therefore fires when linearWidth > 5000e27, and A = 0 is valid (inclusive range 0 to 5000e27). The 2e27 figure is contradicted by the in-source parameter guide, which recommends A ≈ 100e27–300e27 for tight stablecoin pairs.

Depeg behavior (derived from source)

Note that linearWidth is the coefficient A on the linear term, not a literal price "band" — there is no explicit width boundary in the math. "Leaving the band" corresponds to the pool moving out of the linear-dominated near-peg region into the square-root-dominated regime.

The invariant fixes finite reserves. At the initial anchor u = v = 1, the constant is C = 2 + 2A. Draining one side to v = 0 gives √u* + A·u* = 2 + 2A, so the opposite normalized reserve grows to at most u* = 4 when A = 0, and strictly less as A increases. This matches the source overflow comments (u ≤ u* ≤ 4·ONE for any A ≥ 0).

Consequently, as one token is depleted toward zero its marginal price rises steeply toward a hard boundary: the incoming token's reserve can grow to at most ~4× its initial normalization value (exactly 4× at A = 0, tighter for larger A), after which the pool is exhausted. Because the anchor X₀/Y₀ is fixed at build time and reserves are finite, a genuine drifting depeg is not tracked — the pool simply gets drained of the richer asset. This is why the curve is not suitable for drifting-peg assets (e.g. an ever-increasing LST exchange rate); it fits fixed or hardcoded-ratio pairs.

Verified vs unverified

  • Verified: function name/signature/mutability; five uint256 args in order x0, y0, linearWidth, rateLt, rateGt; 32 bytes each; 160-byte total; 1e27 scale; square-root linear curve family with hardcoded p = 0.5; maker-favorable rounding (amountOut down, amountIn up); finite-reserve hard boundary at u* ≤ 4.

  • Corrected: maximum linearWidth is 5000e27 (not 2e27); A = 0 is permitted.

Worked example & scope

This section walks one concrete PeggedSwap parameterization end to end and states what the v1.0.1 pegged strategy does and does not cover. It builds on the args layout and curve verified above; it does not restate them.

linearWidth scale and bound

linearWidth is the linear-term coefficient A, scaled by 1e27. The scale and the upper bound come straight from source:

Constant Value Source
ONE 1e27 PeggedSwapMath.sol line 12
MAX_LINEAR_WIDTH 5000 * ONE = 5000e27 PeggedSwapMath.sol line 14

The instruction rejects any strategy whose coefficient exceeds the bound. In PeggedSwap.sol the validation is:

require(args.linearWidth <= PeggedSwapMath.MAX_LINEAR_WIDTH,
        PeggedSwapInvalidLinearWidth(args.linearWidth));

So the admissible range is inclusive: 0 <= linearWidth <= 5000e27. Passing A as a plain integer (e.g. 100 instead of 100e27) does not revert, but it collapses the linear term to ~zero and the pool prices as a bare square-root curve. Always pre-scale A by 1e27.

The NatSpec on linearWidth reads: "Linear component coefficient A scaled by 1e27 (e.g., 100e27 for A=100); must be <= PeggedSwapMath.MAX_LINEAR_WIDTH". For tight stablecoin pairs the in-source guidance sits well inside the bound (order of 100e27300e27), leaving the 5000e27 ceiling as a hard limit rather than a working value.

A USDC/DAI 1:1 parameterization

Take a maker quoting a 1:1 peg between USDC (6 decimals) and DAI (18 decimals), seeding roughly 1,000 of each and choosing a tight coefficient A = 100. The rate multipliers normalize both tokens to a common 18-decimal base, exactly as the source NatSpec example does (USDC scaled up by 1e12, DAI left as-is):

Field Value How it is derived
rateLt 1e12 USDC (lower-address token here) scaled 6→18 decimals
rateGt 1 DAI (higher-address token) already 18 decimals
x0 1000e6 * 1e12 = 1000e18 USDC seed × rateLt
y0 1000e18 * 1 = 1000e18 DAI seed × rateGt
linearWidth 100 * 1e27 = 100e27 A = 100, pre-scaled by ONE

Which token is tokenLt versus tokenGt is fixed by raw address ordering, not by symbol. The maker assigns rateLt / rateGt and the matching x0 / y0 to whichever token holds the lower address; swap the assignments if DAI is the lower-address token on your chain. The instruction re-orients rates per fill (rateIn = tokenIn < tokenOut ? rateLt : rateGt), so the pair is symmetric once the assignment is correct.

Packed into the program, the five words follow the verified 160-byte layout:

Solidity
1
2
3
4
5
6
7
8
9
10
program.build(_peggedSwapGrowPriceRange2D, PeggedSwapArgsBuilder.build(
    PeggedSwapArgsBuilder.Args({
        x0:          1000e18,   // USDC seed, normalized
        y0:          1000e18,   // DAI seed, normalized
        linearWidth: 100e27,    // A = 100, scaled by ONE (1e27)
        rateLt:      1e12,      // USDC 6 -> 18 decimals
        rateGt:      1          // DAI already 18 decimals
    })
));
// abi.encodePacked -> 5 x 32 bytes = 160 bytes

All three require guards in PeggedSwap.sol pass for these values: x0 > 0 && y0 > 0, linearWidth (100e27) <= 5000e27, and rateLt > 0 && rateGt > 0.

Scope: two-token strategies only

The v1.0.1 pegged instruction is strictly pairwise. _peggedSwapGrowPriceRange2D is a 2D instruction operating on a single tokenLt / tokenGt pair with exactly two reserves (x0, y0) and two rate multipliers. There is no native multi-asset pegged pool in this release — a three-or-more-stablecoin basket is not expressible as one PeggedSwap strategy. A maker who wants basket coverage deploys independent two-token strategies (one per pair) sharing the same wallet allowances.

Scope: a depeg is not auto-guarded

The pegged curve has no oracle, price feed, or circuit breaker. The instruction args are only x0, y0, linearWidth, rateLt, rateGt — nothing references an external price. The curve prices purely from its own reserves and the fixed rateLt / rateGt ratio. If one asset depegs, the pool does not pause or reprice on its own; it keeps quoting along the square-root linear curve and gets arbitraged — the drained-reserve boundary described above is the only intrinsic stop.

Bounding depeg exposure is therefore the maker's responsibility, expressed through the same build-time parameters:

  • rateLt / rateGt pin the target ratio. They are set once at build time and do not drift, so the strategy fits fixed or hardcoded-ratio pairs, not a moving exchange rate.

  • linearWidth (A) sets how tightly the curve hugs the peg. A larger A keeps quotes near 1:1 across a wider reserve range but also tightens the finite-reserve ceiling; a smaller A gives more room before the square-root regime dominates.

  • Seed size / range (x0, y0) caps the notional a taker can extract before the richer asset is drained.

Because the maker's tokens stay in their own wallet under a revocable per-token allowance, the fastest response to a confirmed depeg is off-curve: revoke the allowance to halt fills, then rebuild the strategy with new rates if desired. The curve itself will not do this automatically.

Did you find what you need?