[Proposal] Authorize LidoRelayer

This proposal aims to authorize a relayer for atomically wrapping and unwrapping stETH and wstETH during typical Balancer V2 operations. The relayer would be granted authority to perform the following tasks on behalf of users who have opted into using it:

  • Leverage the user’s existing Vault allowances and internal balances to avoid duplicate approvals.
  • Swap on the user’s behalf.
  • Add and remove liquidity on the user’s behalf.


The upcoming wstETH-WETH meta stable pool is poised to be a huge source of liquidity for Balancer. Lido’s “staked ETH” (stETH) is a yield-bearing ERC-20 token that gives holders exposure to ETH staked in Eth2.0 validators. The design of this pool enables LPs who are long ETH to earn swap fees with minimal impermanent loss risk thanks to the high price correlation of the pool’s assets. Curve Finance has a similar pool with over $3B in liquidity at time of writing.

However, stETH is a rebasing token designed to maintain 1:1 correlation to underlying ETH, and rebasing tokens are incompatible with Balancer. Therefore, the Balancer pool actually contains not stETH but a wrapped version called wstETH which does not rebase. Because most of Lido’s users hold stETH and not wstETH, this represents a UX challenge for Balancer traders, who would need to manually wrap or unwrap tokens before or after trading. The relayer seeking authorization in this proposal is designed to ease that UX burden by automatically wrapping or unwrapping stETH or wstETH within the same transaction as a swap, join, or exit.


This proposal would only grant the required roles to the LidoRelayer contract. Each user would still be required to opt into the relayer by submitting an approval transaction or signing a message.

The code is a simple wrapper over existing Balancer pool interactions, and it has been thoroughly tested and peer-reviewed.


The Balancer governance multisig would submit a transaction to the Authorizer in order to grant the following roles to the LidoRelayer:

  1. manageUserBalance: Utilize existing Vault allowances and internal balances so that a user does not have to re-approve the new relayer for each token.
  2. joinPool: Add liquidity to a pool on the user’s behalf (for the user who holds stETH and wants to deposit to any wstETH pool).
  3. exitPool: Remove liquidity from a pool on the user’s behalf (for the user who wants to withdraw unwrapped stETH from any wstETH pool).
  4. swap: Trade within a single pool on the user’s behalf (for the user who either holds or demands stETH and wants to trade in any wstETH pool).
  5. batchSwap: Make a multihop trade or source liquidity from multiple pools (a more complicated swap, for the same user).

Specifically, the Gnosis Safe at 0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f would send a transaction to the Vault Authorizer at 0xA331D84eC860Bf466b4CdCcFb4aC09a1B43F3aE6 with the following data to authorize the LidoRelayer at 0xdcdbf71A870cc60C6F9B621E28a7D3Ffd6Dd4965:


Which is the ABI-encoded calldata for:

For transparency, a developer could reproduce the encoded data by computing the following:
const ethers = require("ethers")

const authorizer = new ethers.utils.Interface([
  "function grantRoles(bytes32[] memory roles, address account)",
const vault = new ethers.utils.Interface([
  "function manageUserBalance((uint8, address, uint256, address, address)[])",
  "function joinPool(bytes32, address, address, (address[], uint256[], bytes, bool))",
  "function exitPool(bytes32, address, address, (address[], uint256[], bytes, bool))",
  "function swap((bytes32, uint8, address, address, uint256, bytes), (address, bool, address, bool), uint256, uint256)",
  "function batchSwap(uint8, (bytes32, uint256, uint256, uint256, bytes)[], address[], (address, bool, address, bool), int256[], uint256)",

function roleId(address, sighash) {
  return ethers.utils.solidityKeccak256(["uint256", "bytes4"], [address, sighash])

const roles = ["manageUserBalance", "joinPool", "exitPool", "swap", "batchSwap"]
  .map(name => roleId("0xBA12222222228d8Ba445958a75a0704d566BF2C8", vault.getSighash(name)));
const lidoRelayer = "0xdcdbf71A870cc60C6F9B621E28a7D3Ffd6Dd4965";
const data = authorizer.encodeFunctionData("grantRoles", [roles, lidoRelayer]);

    ${roles.map(role => `${role},`).join("\n    ")}
1 Like