Bulkers in Compound V3

The bulker contracts in Compound V3 are multicall-like contracts for batching several transactions.

For example, if we wanted to supply Ether, LINK, and wBTC as collateral and borrow USDC against it in one transaction, we can do that.

We can also reduce the collateral holdings and withdraw a loan in one transaction as the screenshot below demonstrates. This of course assumes that we stay within the collateral factor limits.

bulker example

invoke()

The bulker does not behave like a traditional multicall where it accepts a list of arbitrary calldata. Instead, it takes two arguments: a list of actions (of which there are 6 choices) and the arguments to supply to them. The function is shown below. Specifically, we can

  • supply an ERC 20 (ACTION_SUPPLY_ASSET)
  • supply ETH (ACTION_SUPPLY_NATIVE_TOKEN)
  • transfer an asset (ACTION_TRANSFER_ASSET), see our article on how Compound V3 behaves like a rebasing ERC 20 token to see how this works
  • withdraw an ERC 20 (ACTION_WITHDRAW_ASSET)
  • withdraw ETH (ACTION_WITHDRAW_NATIVE_TOKEN)
  • claim accumulated COMP rewards. The underlying function claimReward will interact with the reward contract instead of the main lending contract (Comet.sol).

invoke function

Invoke will loop through the actions and call Comet (or the rewards contract) with the arguments supplied.

Although it could have been more gas efficient to put this code into the main contract, using msg.value in a loop, especially when invoking delegatecall, is not safe. See the practice problem at the end of this article.

Compound is careful to make sure msg.value is deducted rather than re-used, which could lead to double spending — see the yellow boxes.

This arguably could have been gas optimized by having the actions decided with a 1 byte indicator rather than a 32 byte ascii string indicator.

The bulkers are non-custodial

The bulker never holds the tokens of the user. Instead, the user gives approval to the bulker and the bulker supplies assets to Compound on behalf of the user. Compound does not assume that msg.sender is the depositor; doing so is generally not a good design pattern because it breaks composability — it prevents other contracts from acting on behalf of the user.

Rescuing tokens

In the case where users accidentally transfer tokens to the bulker, an admin can remove the tokens. Note we have separate functions for remove ETH that is stuck and ERC 20 tokens that are stuck.

sweepToken function

Handling non-standard ERC 20 tokens

One of the most common deviations from the ERC 20 token specification is not returning a boolean value. Several ERC 20 tokens do not return a boolean but revert in the failure case.

To handle both situations when dealing with ERC 20 assets, Compound implements the code below:

handling non-standard erc20 tokens

There is no return value of IERC20NonStandard — it will revert if dealing with a non-standard ERC 20 token. To handle the possibility that this is actually a standard ERC 20 token, the case where the return data size is 32 bytes will signify that the token did return a value. If the returned value is 1, it succeeded, and otherwise failed. The different scenarios are captured in the matrix below.

An image of different scenario matrix

Essentially, we succeed on the cases where the (token does not revert AND returns nothing) OR (token does not revert AND returns true). We fail on cases where (token reverts) OR (token does not revert AND returns false).

The IERC20Nonstandard interface simply defines an ERC20 token that does not return any booleans.

nonstandard erc20 interface

Practice problems

Bad things can happen when using msg.value inside a loop. See these two practice problems:

  1. RareSkills Riddles MultiDelegateCall

  2. DamnVulnerableDefi Free Rider

Learn More with RareSkills

Please see our web3 bootcamp to learn more.

Originally Published January 9, 2024

get_D() and get_y() in Curve StableSwap

get_D() and get_y() in Curve StableSwap This article shows algebraically step-by-step how the code for get_D() and get_y() are derived from the StableSwap invariant. Given the StableSwap Invariant: $$ An^n\sum x_i +D=An^nD+\frac{D^{n+1}}{n^n\prod x_i} $$ There are two frequent math operations we wish to conduct with it: Compute $D$ given fixed values for $A$, and the […]

Fixed Point Arithmetic in Solidity (Using Solady, Solmate, and ABDK as Examples)

Fixed Point Arithmetic in Solidity (Using Solady, Solmate, and ABDK as Examples) A fixed-point number is an integer that stores only the numerator of a fraction — while the denominator is implied. This type of arithmetic is not necessary in most programming languages because they have floating point numbers. It is necessary in Solidity because […]

Uniswap V2: Calculating the Settlement Price of an AMM Swap

Uniswap V2: Calculating the Settlement Price of an AMM Swap This article explains how to determine the price settlement of a trading pair in an Automated Market Maker (AMM). It answers the question of “How many token X can be swapped for token Y from the AMM?”. The swap() function on Uniswap V2 requires you […]

How Chainlink Price Feeds Work

How Chainlink Price Feeds Work Chainlink price oracles are smart contracts with public view functions that return the price of a particular asset denominated in USD. Off-chain nodes collect the prices from various sources like exchanges and write the price data to the smart contract. Here is the smart contract for getting the price of […]