19 KiB
19 KiB
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)