Decrease gas fees with shared vaults (or broker contracts)

Note I have limited understanding in how gas expensive is the conversion from straight tokens like DAI, USDC into yVault. So this solution may not work, please let me know.

Summary:

Gas fees are currently a killer for small participants, especially for strategies like DCA (Dollar Cost Averaging). I propose a solution where users pool money and the pool batch deploy tokens into yearn products as if they were a single account.

Specification:

For each yVault there is a contract that has the following methods: deposit(), convert() and withdraw().

  1. The user sends their tokens into the contract via deposit().
  2. Anyone can call convert(), which opens a position in the vault for this shared contract as if it would be a single account.
  3. A user can call withdraw() which would allow the user to take their fair share from the shared vault.

The treasury can daily pay gas to run a convert() if no convert has happened.

Earnings for YFI stakers:
The YFI stakers should take a fee off this service (and with part of the fee they should daily pay a tx fee to convert())

Who pays the gas fees?

  • Simple approach: multisig calls convert once a day
  • Complex approach: treasury allocates some tokens that can be spent into gas for calling convert once every 1 day worth of blocks.
6 Likes

This is an interesting proposal. I wonder how much in added gas expenses this would be? Would it be just the cost of a deposit into the vault for each vault once a day? Seems like it. Also, are you suggesting to batch withdrawals as well to save gas, else Iā€™m not sure I see why else there would be a withdrawal function?

2 Likes

I like the idea! Alternative framing: have a ā€œbrokerā€ contract that can pool peopleā€™s deposits/withdrawals. Make processing the deposits/withdrawals a part of a vaultā€™s harvest.

You could even tokenize claims to the deposits/withdrawals that could be traded or cashed in once the deposit/withdrawal is done.

Example flow: I want to get into the yCRV vault, but itā€™s expensive to directly enter the vault. Instead I deposit into the broker contract, I get pre-yyCRV tokens corresponding to my share of the deposits in the broker. The pre- tokens are erc20s and can be traded and moved around. Once someone triggers the broker contract to process pending deposits/withdrawals, my pre-tokens can be redeemed for yyCRV.

You can imagine a similar flow for withdrawing-- deposit your yyCRV tokens to get post-yyCRV tokens, which settle into yCRV after the broker contract processes.

This takes the yield farming idea of amortizing the cost of harvesting further by amortizing the cost of depositing and withdrawing!

5 Likes

You could also sell your pre-yyCRV (waiting spots) for people that want to get in faster, perhaps another market thereā€¦

This triggers weird tax situations, some users would rather convert than buy (also buying seems to be at a premium)

While convert() will have a O(1) cost, withdrawal will have an O(n) cost. So, withdrawal is per user.

This is how I am planning to build/others to build this. Sorry I omitted low level details but thatā€™s the idea.

While convert() will have a O(1) cost, withdrawal will have an O(n) cost. So, withdrawal is per user.

Mind clarifying what you meant here? I think the withdraws could be batched too, eg. via users depositing their yyCRV (vault shares) and receiving post-yyCRV once the contract redeems its pooled yyCRV for yCRV. Maybe Iā€™m missing something.

In the design Iā€™m describing users would have to redeem their deposit/withdrawals at some point, which is a bit undesirable, but the upshot is that on net weā€™re still saving so much gas across the pooled assets that itā€™s fine. Basically replacing arbitrarily complex vault deposit/withdrawal operations with ~2 ERC20 transfers each.

This is how I am planning to build/others to build this. Sorry I omitted low level details but thatā€™s the idea.

Cool! Iā€™ve been hoping someone would put something like this together-- let me know if I can help write or review any code :grin:

Iā€™m not sure I understand why if you batch deposits that they have to withdrawal with this system? They can just withdrawal via the already build interface no?

2 Likes

Hey sina, I find your comments very valuable, so keep them coming :slight_smile:

I think you are right, there is a way to make withdrawal cheaper, but not sure if the way you are proposing is the one I am thinking about.

Letā€™s find some common definitions so that we can agree.

Old proposal

  • User deposit() DAI into broker contract (Cost: 1)
  • Anyone convert() balance from DAI into yUSDC (Cost: X)
  • User withdraws by converting their own share from yUSDC into DAI (Cost: X)

Costs:

  • The user pays 1 to get in and X to get out
  • Any user (or YFI treasury) pays X to convert

New proposal

  • User deposit(): DAI into broker contract (Cost: 1)
  • User flagsWithdrawal(): flags they are interested in exiting the pool (Cost: 1)
  • Anyone convert(): (Cost: X)
    • Convert all DAI balance into yUSD
    • Convert all the yUSD flagged from withdrawal into DAI
  • User claim() to transfer balance to themselves (Cost: 1)

Costs:

  • The user pays 1 to get in, 1 to flag they want to get out, 1 to claim their money
  • Any user (or a YFI treasury) pays X to convert the broker account from DAI to yUSD and viceversa.

Note that I still think that the old proposal may result in an overall cheaper gas if users are not interested in getting out straightaway.

Let me know if you are thinking something else

No, they cannot, the ā€œbroker accountā€ is depositing money for them. A user has a share of the broker account balance in the yVault.

This is already how yVaults work. Tokens are stored in the vault when deposit() is called by the user, and then put to work whenever earn() is called by the deployer.

No, I think you may be missing the peculiar aspect of this proposal. The deposit into the vault is the expensive part of the gas and itā€™s paid per-user. This proposal aims at having very simple ERC20 transactions to a broker contract and this gas is to be paid by the user, the broker contract then calls the vault deposit and pays the gas for everyone.

I may still misunderstand what is going on.

1 Like

So I have another idea for this that could perhaps help save even more gas.

The broker contract only goes one way daily. It either withdraws, or it deposits, based on the cumulative requests in the past 24 hours.

So say 100k DAI is sent to deposit, and 50k DAI is requested to withdraw from the poolā€“ instead of doing both a DAI -> yUSD for 100k and a 50k yUSD -> DAI, it simply does a 50k DAI -> yUSD deposit and the distributes the remaining 50k DAI from depositors to withdrawers.

Iā€™m not a dev so I donā€™t know whether this would actually save on gas costs, but to me it seems like it theoretically would since itā€™s eliminating one extra in/out of the vaults. Perhaps the best aspect of this, is that if deposits were higher than withdrawals that day, then there would be no 0.5% withdrawal fee!

2 Likes

That seems really cool! Even though depositing to yVaults are quite affordable, I see the case for optimizing that, via implementing a ā€˜waiting listā€™ broker contract for pool participants to share the costs going from DAI -> yVaults.

There is a yVault yCrv recycler UI by x48, for the unaudited contract by banteg, on which this concept can be built upon, since the vault recycler does saves more gas than using zapper directly.

Link to Yearn Vault yCrv recycler UI:
https://vaults.finance

I like this idea a lot actually. I know that gas is much cheaper for simple send transactions, and the idea behind yearn is to save on gas fees.

With a broker contract, could this potentially open up the door for Layer 2 scaling into Yearn right now?

Projects like Loopring and ZKSync are usable right now for only simple send and receive transactions, not interacting with smart contracts (Not quite yet anyways). However gas fees are magnitudes cheaper on these second layers.

I think this is something worth looking into

To be fair, I donā€™t think the work of integrating an L2 solution for sends (if only doing them once a day) really makes sense. Youā€™d be saving like $10/day max (and this is for the pooled contract). Now, once smart contract interactions hop onto layer 2, that will make some REAL savings.

Thatā€™s a fair point, however, it would allow a lot smaller amounts to be accumulated more often without high gas cost. And also if the balances grow fast enough on a daily basis, like say if the cost of gas to deposit to the vault drops below 1% of the pool contract balance, there could be a savings of more than $10/day if there are multiple deposits.

It would lower the cost barrier of entry even further everyone, it really just depends on how much demand there is I suppose

But this is something the Yearn community could charge a fee for since it doesnā€™t yet exist anywhere else

Depositing into the yVault isnā€™t particularly expensive. The earn() call does the brunt of the work. I doubt youā€™d save much gas by aggregating.

Hereā€™s the code for the deposit function. Itā€™ll be tough to write it much more efficiently:

function deposit(uint _amount) external {
      uint _pool = balance();
      token.safeTransferFrom(msg.sender, address(this), _amount);
      uint shares = 0;
      if (_pool == 0) {
        shares = _amount;
      } else {
        shares = (_amount.mul(totalSupply())).div(_pool);
      }
      _mint(msg.sender, shares);
      }
4 Likes

Thanks for the code pointer, and I agree with you it looks like the deposit function is quite sleek already. The only thing I can think of is the balance view function being calculated for every deposit, which has to do a bunch of nested calls, but my instinct is this doesnā€™t cost too much gas.

The withdrawal function looks like it could benefit from the methods discussed in this post though:

function withdraw(uint _shares) public {
      uint r = (balance().mul(_shares)).div(totalSupply());
      _burn(msg.sender, _shares);

      // Check balance
      uint b = token.balanceOf(address(this));
      if (b < r) {
          uint _withdraw = r.sub(b);
          Controller(controller).withdraw(address(token), _withdraw);
          uint _after = token.balanceOf(address(this));
          uint _diff = _after.sub(b);
          if (_diff < _withdraw) {
              r = b.add(_diff);
          }
      }

      token.safeTransfer(msg.sender, r);
}

It looks like the vault already matches a user attempting to withdraw with pending deposits, which is great. But for a user withdrawing an amount greater than the pending deposits, theyā€™ll have to withdraw from the controller, which withdraws from the strategy, which, depending on the strategy, may be expensive.


Furthermore, based on how the system works right now (not harvesting upon each deposit/withdrawal), it seems thereā€™s a subtle (and not severe) ā€œhackā€ a user could do to withdraw more interest than theyā€™ve earned. Deposits are given shares equal to the current balance of the vault, but this may not necessarily reflect the accrued interest thatā€™s pending being rotated back into the base crop. Specifically, a user could take the following sequence of actions to get slightly more returns than they should (using yWETH vault in my example):

  1. A user named Eve waits until right before they anticipate harvest being called. Say harvest on average rotates 10% of the vaultā€™s balance as interest back into the base crop (ie. selling accrued CRV for WETH, or whatever). Say harvest is called on average once per hour.
  2. Eve deposits a bunch of WETH. The way deposit works right now, Eve would get yWETH vault shares based off the current balances of ETH, but not accounting for pending accrued interest.
  3. harvest is called 1 minute after Eveā€™s deposit. Eveā€™s pool shares increase in value based on the accrued interest from the whole past hour, rather than just the past 1 minute.
  4. Eve withdraws 1 minute after harvest.

After this sequence of events, Eve only had her money in the vault for ~2 minutes, but she accrued interest for the entire past hour.

A system with a more explicit layer of a ā€œbrokerā€ contract on top could solve this by having deposits and withdrawals being a part of harvest. End-users interact with the broker contract to stage their deposits/withdrawals, and then harvest processes them immediately after rotating any pending interest back to the desired underlying asset.

Sorry if this is wordy or hard to follow-- let me know if I can clarify or if Iā€™m missing something obvious that makes this work differently than Iā€™m describing.

Contract addresses for reference:

1 Like