[BIP-299] Permissioned arbitrage to recover remaining funds in bb-e-usd

Payload

TL;DR

  • Inverse Finance has around bb-e-usd representing around $300,000 of the around $320,000 in liquid value still held in bb-e-usd and rightfully owned by remaining bb-e-usd holders. The inverse bb-e-usd is in a contract that will not allow them to withdraw using methods so far provided.
  • bb-e-usd and other linear pools will unpause on June 8th as the grace period on the pause window comes to an end. This will allow arbitrageurs to drain remaining value from the pool.
  • This BIP is a actions a plan to unpause the pools early, and run the arbitrage.
    • Once the exact amount recovered is known, another BIP will be brought forward to proportionally distribute the recovered funds to remaining bb-e-usd holders as of the block of the Arbitrage.
  • After this point, bb-e-usd burns will render a value of almost 0 and will in no case be worth the gas.
  • Special thanks to TempleDAO for providing the eDAI and eUSDC required to run this process.

Background/Context

As initially described in this RFC. There is still a significant amount of value left in bb-e-usd, a vast majority of which belongs to Inverse Finance.

Since the time of the RFC posted above, The Balancer Maxis have spent time working with TempleDAO, Euler, and Inverse to ensure that all required conditions are met on-chain to run the arb. Euler patched a contract, Temple sent eTokens to the Balancer DAO Multisig.

The PoC has been improved to use a smart contract, deployed here. This removes most of the issues that could occur between the time the payload was generated and the time the transaction is executed(about a week for governance).

Since the RFC, it was also decided that it would be safer/better not to try to atomically distribute the proceeds from the arbitrage in the same transaction as the arbitrage itself, and instead to do a second BIP/execute at a later date.

Specification

If this BIP is passed, the attached payload will be executed as soon as possible.

It takes the following steps:

  1. Grants unpause permissions to Euler Linear Pools to the DAO Multisig.
  2. Unpauses bb-e-usdc and bb-e-dai
  3. Internally transfers all of the eDAI and eUSDC in the DAO Multisig to the helper contract 0xF23d8342881eDECcED51EA694AC21C2B68440929
  4. calls the do_arb function on the smart contract in order to activate the arbitrage
  5. Removes pause rights from the DAO multisig.

The Pause window is now in a grace period, so it is not possible to repause the pools. Following execution of this BIP bb-e-usdc, bb-e-dai will be in an unpaused state.

Important Note

The payload provided is slightly sensitive to changes in market conditions. As such there is a dust factor. This changes the amount of funds left in the pool to handle paying fees for trading the “wrong direction” in an off balance linear pool. Note that these fees could only be collected by an arb who rebalanced the pool, which does not seem very possible/sensible so they should be considered lost value.

Inverse is clearly very concerned about recovering their coins. The Maxi’s therefore ask permission to recheck this payload before it is loaded on the DAO multisig, and in the event of a failed simulation, update the dust factors, currently set to [350,300] as required. No other changes to the payload will be made.

Onward steps

Following execution of this BIP, governance will be brought forward to distribute the proceeds, and internally transfer remaining eTokens back to the TempleDAO multisig.

Review Requirements

Note that the payload and contract submitted have not been reviewed by anyone outside the Maxis. This BIP will not be executed until at least 1 review by a trusted and skilled engineers outside the Maxis is posted in response to this forum post. We welcome anyone interested to complete and post such a review. A request has been submitted to the Integrations Team to provide said review.

More about Pausing

New Balancer pool types and contracts generally include a 90 day Pause window as part of the launch process. If Pause is called during this time, a 90 day grace period kicks in, after which point the pool automatically unpauses. The Euler linear pools had been deployed less than 90 days before the Euler exploit making the pause possible. Following the end of the pause window, pausing of the pool is no longer possible.

You can read more about pausing here:

Note that since this time, pause rights have moved from the blabs ops multisig to the Emergency SubDAO

A full list of current mainnet permissions can be found here:

4 Likes

https://snapshot.org/#/balancer.eth/proposal/0xcf116e440ae71ea05502c10930efb90e294af63b1d9333924ef905314f5f7004

Hey, lead solidty dev at Inverse Finance here.

I’ve gone over the rescue contract code and transaction payload and it looks good to me.

I’ve written a foundry forge test for simulating the transaction payload that can be found at:

And run with forge test --fork-url <Your RPC provider> -vvv

Thank you for your work on this!

3 Likes

Hi everyone, the Integrations Team also reviewed this last week. @joaobrunoah, @gerg, and myself each put eyes on the code. We agree that the do_arb function, in the context of the specified Gnosis transaction batch, is safe to execute. We also had the following comments which amount to an informational QA report; none of the issues mentioned has a material impact on execution when viewed in the context of this specific payload. Line numbers and function names refer to the bbeUSD_arb contract.

Trust Assumptions

All external functions are marked onlyOwner, so only the current owner or any future owner (after ownership transfer) has the power to execute. If we trust the current owner to properly execute and not to transfer ownership to a malicious entity, then we can trust the contract.

Quality Assurance Report

  • The do_arb function will fail for all pools if any one of the provided inputTokens[i] has a 0 balance or doesn’t contain the specified outputTokens[i].
  • For governance transparency, the recipient address (do_arb) and payee addresses (withdraw and sweep) should be immutable: either hard-coded or specified at construction time.
  • There is a comment implying that this is a single-use contract but nothing to enforce it.
  • The sweep function should utilize safeTransfer to account for tokens like USDT (out of scope for the specified payload).
  • The deadline (block.timestamp + 30) on line 43 represents an infinite deadline. To specify a real deadline, the timestamp must be passed into this contract from off chain. For an infinite deadline, prefer something like type(uint256).max for clarity.
  • <payable>.transfer(amount) can fail. It is preferable to use (bool sent, bytes memory data) = <payable>.call{value: amount}, but if the payee is expected to be an EOA or a contract with minimal receive logic, it is fine.
  • do_arb needs more comments (NatSpec) to explain what the function does and the main parts of its code. It’s unclear when reading it and even harder to understand if the contract does what the author intended, because author’s intentions are not documented in the code.
  • inputTokens should be renamed to linearPools or similar to distinguish it from a generic array of ERC-20 tokens.
  • lt (line 25) should be renamed to linearPool or another more descriptive name.
  • usdToken (line 26) is not used and should be removed.
  • foo (line 27) is not used and should be removed.
  • 10 ** 50 (line 43) seems arbitrary and could be replaced by type(uint256).max.
  • outputTokens is unnecessary because we already validate it against linearPool.mainToken(). Just use that getter directly.
  • dustFactors is a strange way to fuzz the output — could alternatively calculate the amount of fees the pool will collect as it’s drained.
  • The payee in the internal_sweep function does not need to be payable since it does not handle ETH.
  • The @author tag misspells one of the author’s names.

Gas Optimizations

  • inputTokens, outputTokens and dustFactor can be calldata instead of memory.
  • In the for loops, change i++ to ++i.
  • Instead of calling lt.getPoolId() 3 times, use the poolId variable from line 38.
  • The whole do_arb function can be rewritten to use a single batchSwap instead of multiple swaps:
before loop:
    create empty array of assets (IAssets)
    create empty array of swaps (BatchSwapStep)
    create empty array of limits (uint256)
in loop:
    add in/out tokens to assets array
    populate BatchSwapStep array w/ indices of in/out tokens
    populate limits
after loop:
    populate FundManagement struct
    set deadline (see comment above about block.timestamp being useless as a deadline)
    execute batchSwap
9 Likes