smart-contracts/backlog/docs/INTEGRATION_REFERENCE.md

19 KiB
Raw Blame History

Balancer V3 & Gyroscope: Integration Reference

Companion to: BALANCER_V3_GYROSCOPE_RESEARCH.md Purpose: Concrete contract addresses, code snippets, and deployment examples


Part 1: Balancer V3 Contract Addresses

Ethereum Mainnet

Vault (Core):                0xBA12222222228d8Ba445958a75a0704d566BF2C8
WeightedPoolFactory:         0x201efd508c8DfE9DE1a13c2452863A78CB2a86Cc
StablePoolFactory:           0xf3de90B4dDD9b3ba9d2ab9BbBe6EfFcf52F1a1fB
Router (EntryPoint):         0xBA12222222228d8Ba445958a75a0704d566BF2C8
ProtocolFeeController:       0xF4c873f02aF4aE79d01aF27Cfd8F25FCC22d9D47

Key Insight: The vault address is the same for all operations. Always target 0xBA12... for swaps/joins/exits.

Arbitrum

Vault:                       0xBA12222222228d8Ba445958a75a0704d566BF2C8
WeightedPoolFactory:         0x86F5453991fBBFF1eDF663108f302a1eB4FdDf10
StablePoolFactory:           0x6B7b84d4bffad1c5C1eC8b3d8f1F5c81c5f1D2a4
GyroECLPFactory:             0x65755d39C36... (check Balancer UI for current)

Base

Vault:                       0xBA12222222228d8Ba445958a75a0704d566BF2C8
WeightedPoolFactory:         0xf3de90B4dDD9b3ba9d2ab9BbBe6EfFcf52F1a1fB
StablePoolFactory:           0x6B7b84d4bffad1c5C1eC8b3d8f1F5c81c5f1D2a4
USDC/sUSDS Boosted Pool:     0x... (look up on Balancer UI)

Note: Factory addresses change across deployments. Always verify on Balancer Deployments repo or Balancer UI for current addresses.


Part 2: Python → Solidity Translation Guide

E-CLP Parameters

Python (elliptical_clp.py:33-43):

@dataclass
class ECLPParams:
    alpha: float        # Lower price bound
    beta: float         # Upper price bound
    c: float           # cos(-φ) rotation
    s: float           # sin(-φ) rotation
    lam: float         # λ ≥ 1, stretching factor

Solidity (FixedPoint 18 decimals):

struct ECLPParams {
    uint256 alpha;      // e.g., 0.95e18 (95% price)
    uint256 beta;       // e.g., 1.05e18 (105% price)
    uint256 c;          // e.g., 0.707107e18 (cos 45°)
    uint256 s;          // e.g., 0.707107e18 (sin 45°)
    uint256 lam;        // e.g., 2.0e18 (2x stretch)
}

// Validation
require(alpha < beta, "α must be < β");
require(lam >= 1e18, "λ must be ≥ 1");
uint256 norm = sqrt(c*c + s*s);
require(abs(norm - 1e18) < 1e8, "(c,s) must be unit");

Invariant Computation

Python (elliptical_clp.py:106-150):

def compute_invariant(x: float, y: float, p: ECLPParams, dp: ECLPDerivedParams) -> float:
    A = make_eclp_A(p.c, p.s, p.lam)

    AChi_x = dp.w / p.lam + dp.z
    AChi_y = p.lam * dp.u + dp.v
    AChi = np.array([AChi_x, AChi_y])
    AChi_sq = AChi @ AChi

    t = np.array([x, y])
    At = A @ t
    At_sq = At @ At

    AtAChi = At @ AChi
    denom = AChi_sq - 1.0

    discriminant = AtAChi**2 - denom * At_sq
    r = (AtAChi + np.sqrt(discriminant)) / denom
    return r

Solidity (PRBMath fixed-point):

import { PRBMath } from "@prb/math/PRBMath.sol";

function computeInvariant(
    uint256 x,
    uint256 y,
    ECLPParams memory params,
    ECLPDerivedParams memory derived
) internal pure returns (uint256 invariant) {
    // Compute A-matrix
    (uint256 a00, uint256 a01, uint256 a10, uint256 a11) = _computeAMatrix(
        params.c, params.s, params.lam
    );

    // AChi components
    uint256 AChi_x = PRBMath.mulDiv(derived.w, 1e18, params.lam) + derived.z;
    uint256 AChi_y = PRBMath.mulDiv(params.lam, derived.u, 1e18) + derived.v;

    uint256 AChi_sq = PRBMath.mulDiv(AChi_x, AChi_x, 1e18) +
                      PRBMath.mulDiv(AChi_y, AChi_y, 1e18);

    // At = A @ (x, y)
    uint256 At_x = PRBMath.mulDiv(a00, x, 1e18) + PRBMath.mulDiv(a01, y, 1e18);
    uint256 At_y = PRBMath.mulDiv(a10, x, 1e18) + PRBMath.mulDiv(a11, y, 1e18);

    uint256 At_sq = PRBMath.mulDiv(At_x, At_x, 1e18) +
                    PRBMath.mulDiv(At_y, At_y, 1e18);

    // AtAChi = At · AChi
    uint256 AtAChi = PRBMath.mulDiv(At_x, AChi_x, 1e18) +
                     PRBMath.mulDiv(At_y, AChi_y, 1e18);

    // Quadratic: denom * r^2 - 2*AtAChi*r + At_sq = 0
    int256 denom = int256(AChi_sq) - int256(1e18);

    if (denom == 0) {
        // Degenerate: circle case λ=1
        return _sqrt(At_sq);
    }

    // discriminant = AtAChi^2 - denom * At_sq
    uint256 disc = PRBMath.mulDiv(AtAChi, AtAChi, 1e18) -
                   PRBMath.mulDiv(uint256(abs(denom)), At_sq, 1e18);

    // r = (AtAChi + sqrt(disc)) / denom
    uint256 sqrtDisc = _sqrt(disc);
    invariant = PRBMath.mulDiv(AtAChi + sqrtDisc, 1e18, uint256(abs(denom)));

    return invariant;
}

function _computeAMatrix(
    uint256 c, uint256 s, uint256 lam
) internal pure returns (uint256 a00, uint256 a01, uint256 a10, uint256 a11)
{
    // A = [[c²/λ + s², (1-c²)(1-1/λ²)],
    //      [(1-c²)(1-1/λ²), s²/λ + c²]]
    // Simplified from GyroECLPMath.sol

    uint256 lam_sq = PRBMath.mulDiv(lam, lam, 1e18);
    uint256 lam_inv = PRBMath.mulDiv(1e18, 1e18, lam); // 1/λ

    uint256 c_sq = PRBMath.mulDiv(c, c, 1e18);
    uint256 s_sq = PRBMath.mulDiv(s, s, 1e18);

    a00 = PRBMath.mulDiv(c_sq, lam_inv, 1e18) + s_sq;
    a11 = PRBMath.mulDiv(s_sq, lam_inv, 1e18) + c_sq;

    uint256 term = (1e18 - c_sq) * (1e18 - lam_inv) / 1e18;
    a01 = term;
    a10 = term;
}

function _sqrt(uint256 x) internal pure returns (uint256) {
    if (x == 0) return 0;
    return PRBMath.sqrt(x);
}

Swap Out Given In

Python (elliptical_clp.py:172-189):

def calc_out_given_in(
    x: float, y: float, p: ECLPParams, dp: ECLPDerivedParams,
    amount_in: float, token_in: int = 0,
) -> float:
    r = compute_invariant(x, y, p, dp)

    if token_in == 0:
        new_x = x + amount_in
        new_y = _calc_y_given_x(new_x, r, p, dp)
        return y - new_y
    else:
        new_y = y + amount_in
        new_x = _calc_x_given_y(new_y, r, p, dp)
        return x - new_x

Solidity (for IBasePool.onSwap):

function onSwap(PoolSwapParams calldata params)
    external
    view
    override
    returns (uint256 amountCalculated)
{
    uint256[] memory balances = params.balances;
    uint256 x = balances[0];  // Token 0 balance
    uint256 y = balances[1];  // Token 1 balance

    uint256 invariant = computeInvariant(x, y, eclpParams, eclpDerived);

    if (params.kind == SwapKind.EXACT_IN) {
        if (params.tokenInIndex == 0) {
            uint256 new_x = x + params.amountIn;
            uint256 new_y = _solveYGivenX(new_x, invariant);
            amountCalculated = y > new_y ? y - new_y : 0;
        } else {
            uint256 new_y = y + params.amountIn;
            uint256 new_x = _solveXGivenY(new_y, invariant);
            amountCalculated = x > new_x ? x - new_x : 0;
        }
    } else {
        // EXACT_OUT: solve for input required
        if (params.tokenOutIndex == 0) {
            uint256 new_x = x - params.amountOut;
            uint256 new_y = _solveYGivenX(new_x, invariant);
            amountCalculated = new_y > y ? new_y - y : 0;
        } else {
            uint256 new_y = y - params.amountOut;
            uint256 new_x = _solveXGivenY(new_y, invariant);
            amountCalculated = new_x > x ? new_x - x : 0;
        }
    }
}

Part 3: P-AMM (Redemption Curve) Hook Example

Simplified Hook Implementation

pragma solidity ^0.8.24;

import { IHooks } from "@balancer-labs/v3-vault/contracts/interfaces/IHooks.sol";

contract SimpleRedemptionHook is IHooks {
    struct PAMMParams {
        uint256 alpha_bar;       // Curvature: 10.0e18
        uint256 xu_bar;          // No-discount threshold: 0.8e18
        uint256 theta_bar;       // Floor rate: 0.5e18
        uint256 outflow_memory;  // Decay: 0.999e18
    }

    struct PAMMState {
        uint256 reserve_value;   // Total USD in reserve
        uint256 myco_supply;     // Total MYCO issued
        uint256 cumulative_redeemed;
        uint256 last_redemption_time;
    }

    PAMMParams public params;
    PAMMState public state;
    address public bondedPool;
    address public governance;

    event RedemptionRateUpdated(uint256 newRate, uint256 timestamp);

    modifier onlyGovernance() {
        require(msg.sender == governance, "Not governance");
        _;
    }

    function initialize(
        uint256 reserve,
        uint256 supply,
        address pool,
        address gov
    ) external {
        state = PAMMState(reserve, supply, 0, block.timestamp);
        params = PAMMParams(10.0e18, 0.8e18, 0.5e18, 0.999e18);
        bondedPool = pool;
        governance = gov;
    }

    function computeRedemptionRate(uint256 redemption_amount)
        public
        view
        returns (uint256)
    {
        if (state.myco_supply == 0) return 1e18;

        uint256 ba = (state.reserve_value * 1e18) / state.myco_supply;

        // Fully backed
        if (ba >= 1e18) {
            return 1e18;
        }

        // No backing
        if (ba == 0) {
            return params.theta_bar;
        }

        // Compute alpha (curvature)
        uint256 delta = 1e18 - ba;
        uint256 alpha = (params.alpha_bar * 1e18) / state.myco_supply;
        if (delta > 0) {
            uint256 min_alpha = 2 * delta;
            alpha = alpha > min_alpha ? alpha : min_alpha;
        }

        // Compute xu (no-discount zone)
        uint256 xu = params.xu_bar;
        if (alpha > 0 && delta > 0) {
            // xu_from_delta = 1.0 - sqrt(2*delta/alpha)
            uint256 ratio = (2 * delta * 1e18) / alpha;
            uint256 sqrt_ratio = _sqrt(ratio);
            xu = (1e18 - sqrt_ratio) < params.xu_bar
                ? (1e18 - sqrt_ratio)
                : params.xu_bar;
        }

        // Current redemption level
        uint256 x = ((state.cumulative_redeemed + redemption_amount / 2) * 1e18) /
                    state.myco_supply;

        // Determine zone
        if (x <= xu) {
            // No discount
            return ba;
        }

        // Parabolic discount (simplified)
        // rate = ba - x + alpha * (x - xu)^2 / 2
        uint256 discount = (alpha * (x - xu) * (x - xu)) / (2e18);
        uint256 rate = ba > x ? ba - x : 0;
        rate = rate + discount / 1e18;
        rate = rate < params.theta_bar ? params.theta_bar : rate;
        return rate > 1e18 ? 1e18 : rate;
    }

    // Hook interface implementations

    function onRegister(
        address pool,
        address factory,
        bytes memory userData
    ) external returns (bytes4) {
        require(pool == bondedPool, "Unknown pool");
        return IHooks.onRegister.selector;
    }

    function beforeSwap(BeforeSwapParams calldata params)
        external
        override
        returns (bytes memory hookData)
    {
        // If swapping OUT of MYCO (redemption), apply discount
        uint256 redemptionRate = computeRedemptionRate(params.amount);
        return abi.encode(redemptionRate);
    }

    function afterSwap(AfterSwapParams calldata params)
        external
        override
    {
        // Update state: flow tracking, backing ratio
        uint256 time_diff = block.timestamp - state.last_redemption_time;
        uint256 decay = (params.outflowMemory ** time_diff) / 1e18;
        state.cumulative_redeemed = (state.cumulative_redeemed * decay) / 1e18;
        state.last_redemption_time = block.timestamp;

        emit RedemptionRateUpdated(
            computeRedemptionRate(params.amountOut),
            block.timestamp
        );
    }

    function beforeAddLiquidity(BeforeAddLiquidityParams calldata)
        external
        pure
        override
        returns (bytes memory)
    {
        return "";
    }

    function afterAddLiquidity(AfterAddLiquidityParams calldata)
        external
        pure
        override
    {}

    function beforeRemoveLiquidity(BeforeRemoveLiquidityParams calldata)
        external
        pure
        override
        returns (bytes memory)
    {
        return "";
    }

    function afterRemoveLiquidity(AfterRemoveLiquidityParams calldata)
        external
        pure
        override
    {}

    // Governance functions
    function updateReserve(uint256 newValue) external onlyGovernance {
        state.reserve_value = newValue;
    }

    function updateParams(
        uint256 alpha,
        uint256 xu,
        uint256 theta
    ) external onlyGovernance {
        params.alpha_bar = alpha;
        params.xu_bar = xu;
        params.theta_bar = theta;
    }

    // Helper
    function _sqrt(uint256 x) internal pure returns (uint256) {
        if (x == 0) return 0;
        uint256 z = (x + 1) / 2;
        uint256 y = x;
        while (z < y) {
            y = z;
            z = (x / z + z) / 2;
        }
        return y;
    }
}

Part 4: Pool Factory Example

pragma solidity ^0.8.24;

import { BasePoolFactory } from "@balancer-labs/v3-pool-utils/contracts/BasePoolFactory.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import { IHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IHooks.sol";
import "./ECLPBondingCurvePool.sol";

contract ECLPBondingCurveFactory is BasePoolFactory {
    event PoolCreated(
        address indexed pool,
        string name,
        address[] tokens,
        uint256[] params
    );

    constructor(IVault vault) BasePoolFactory(vault) {}

    function create(
        string memory name,
        string memory symbol,
        address[] calldata tokens,
        uint256 swapFeePercentage,
        address hooksContract,
        bytes calldata eclpParamsEncoded
    ) external returns (address) {
        require(tokens.length == 2, "Must be 2-token pool");

        // Decode E-CLP params
        (uint256 alpha, uint256 beta, uint256 c, uint256 s, uint256 lam) = abi.decode(
            eclpParamsEncoded,
            (uint256, uint256, uint256, uint256, uint256)
        );

        bytes memory creationCode = type(ECLPBondingCurvePool).creationCode;
        bytes memory constructorArgs = abi.encode(
            address(_vault),
            name,
            symbol,
            tokens,
            swapFeePercentage,
            hooksContract,
            alpha,
            beta,
            c,
            s,
            lam
        );

        address pool = _create(creationCode, constructorArgs);

        emit PoolCreated(
            pool,
            name,
            tokens,
            abi.decode(eclpParamsEncoded, (uint256[]))
        );

        return pool;
    }
}

Part 5: Testing (Foundry)

Test E-CLP Invariant Preservation

pragma solidity ^0.8.24;

import "forge-std/Test.sol";
import "../src/ECLPBondingCurvePool.sol";

contract ECLPBondingCurvePoolTest is Test {
    ECLPBondingCurvePool pool;

    function setUp() public {
        // Deploy pool with test params
        // α = 0.95, β = 1.05, (c,s) = (0.707, 0.707), λ = 1.5
    }

    function testInvariantPreservationOnSwap() public {
        uint256 x = 1000e18;
        uint256 y = 1000e18;
        uint256 invariant1 = pool.computeInvariant(x, y);

        uint256 amountIn = 100e18;
        uint256 amountOut = pool.onSwap(
            PoolSwapParams({
                kind: SwapKind.EXACT_IN,
                tokenInIndex: 0,
                tokenOutIndex: 1,
                amount: amountIn,
                balances: [x + amountIn, y - amountOut],
                // ... other params
            })
        );

        uint256 newX = x + amountIn;
        uint256 newY = y - amountOut;
        uint256 invariant2 = pool.computeInvariant(newX, newY);

        // Invariant should be approximately preserved (allow small rounding error)
        assertApproxEqRel(invariant1, invariant2, 1e15); // 0.0001% tolerance
    }

    function testSpotPriceMonotonic() public {
        // Increasing X should increase Y price (spot price of Y in X)
        uint256 price1 = pool.spotPrice(1000e18, 1000e18);
        uint256 price2 = pool.spotPrice(2000e18, 500e18);

        // With asymmetric ellipse, price moves smoothly
        assertTrue(price2 < price1); // Y becomes cheaper as X increases
    }
}

Part 6: Deployment Checklist

Pre-Launch (Testnet)

[ ] Deploy factory on testnet
[ ] Create sample pool via factory
[ ] Register pool with vault
[ ] Link hooks contract
[ ] Run full test suite (Foundry)
[ ] Compare gas to reference pools (WeightedPool, StablePool, GyroECLP)
[ ] Run fuzz tests (Echidna or Foundry)
[ ] Verify against Python reference implementation
[ ] Integration test with Balancer UI (swaps, joins, exits)

Pre-Launch (Mainnet)

[ ] Security audit (Trail of Bits, OpenZeppelin, etc.)
[ ] Bug bounty program ($50K+ coverage)
[ ] Community review period (1+ week)
[ ] Governance vote (if DAO-controlled)
[ ] Reserve initialization (fund vaults)
[ ] Set conservative swap fee (e.g., 0.5%)
[ ] Low TVL cap (e.g., $100K initial)

Launch Day

[ ] Deploy factory + pools
[ ] Mint initial liquidity
[ ] Announce on Balancer Discord/Forums
[ ] Enable on Balancer UI
[ ] Monitor TVL, gas, price stability
[ ] Be ready to pause if needed (governance power)

Part 7: Example Deployments

Community Treasury (Weighted Pool)

// Using Balancer SDK

const MYCO = "0x..."; // Your token
const USDC = "0x..."; // Stablecoin

const factory = IWeightedPoolFactory("0x201efd508c8DfE9DE1a13c2452863A78CB2a86Cc");

const pool = factory.create(
  "MYCO Treasury LP",
  "MYCO-LP",
  [MYCO, USDC],
  ["600000000000000000", "400000000000000000"], // 60%, 40%
  "5000000000000000", // 0.5% swap fee
  "0x0000000000000000000000000000000000000000" // no hooks
);

// Mint initial liquidity
await vault.joinPool(pool, MYCO, USDC, amounts);

Token Launch (E-CLP + P-AMM Hook)

const LAUNCH_TOKEN = "0x...";
const STABLECOIN = "0x...";

// Deploy hook first
const pammHook = new SimpleRedemptionHook.deploy(
  /* reserve, supply, pool, governance */
);

// Deploy E-CLP via factory
const eclpFactory = ECLPBondingCurveFactory("0x...");

const params = {
  alpha: "950000000000000000", // 0.95
  beta: "1050000000000000000", // 1.05
  c: "707107000000000000",     // cos(45°)
  s: "707107000000000000",     // sin(45°)
  lam: "2000000000000000000"    // 2.0 (asymmetric)
};

const pool = eclpFactory.create(
  "LAUNCH E-CLP",
  "LAUNCH-LP",
  [LAUNCH_TOKEN, STABLECOIN],
  "10000000000000000", // 1% launch fee (higher for illiquid)
  pammHook.address,
  abi.encode(params)
);

// Seed initial liquidity
// Ratio biased toward stablecoin (due to λ=2 stretch)