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
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 |
Related
PeggedSwap: instruction args and curve math
Constant Product: volatile pair variant (
xyc)Concentrated Liquidity: bounded price range
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)
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
uint256args in orderx0, y0, linearWidth, rateLt, rateGt; 32 bytes each; 160-byte total;1e27scale; square-root linear curve family with hardcodedp = 0.5; maker-favorable rounding (amountOutdown,amountInup); finite-reserve hard boundary atu* ≤ 4.Corrected: maximum
linearWidthis5000e27(not2e27);A = 0is 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 100e27–300e27), 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:
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/rateGtpin 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 largerAkeeps quotes near 1:1 across a wider reserve range but also tightens the finite-reserve ceiling; a smallerAgives 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.