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.
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 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.
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.
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:
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.
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.
Bad things can happen when using msg.value inside a loop. See these two practice problems:
Learn More with RareSkills
Please see our web3 bootcamp to learn more.