maple-labs / trail-of-bits-audit

This repo will be used for all communications between the Maple smart contracts team and the Trail of Bits auditing team.
0 stars 0 forks source link

Why do ERC20Helper functions not revert on failure? #12

Open snd opened 2 years ago

snd commented 2 years ago

As opposed to their OpenZeppelin counterparts the functions in ERC20Helper don't revert on failure.

This makes them harder to use correctly. Developers MUST wrap every call to a function in ERC20Helper in a require statement. Forgetting to do so could introduce critical issues.

Is there a specific reason why this behavior was chosen?

Without further context, we'd highly recommend making ERC20Helpers functions revert on failure.

The first transfer in the Liquidator is not wrapped in a require: https://github.com/maple-labs/liquidations/blob/35c628e5ab45fbffaab7aef43a030a98b712a94a/contracts/Liquidator.sol#L42 Is this intentional?

At first glance this seems to enable the following scenario:

  1. user calls Liquidator.liquidatePortion(swapAmount, "")
  2. collateralAsset.balance(Liquidator) < swapAmount i.e. the liquidator has not enough collateral left
  3. ERC20Helper.transfer(collateralAsset, msg.sender, swapAmount_) returns false i.e. fails silently
  4. the user pays for the collateral but doesn't get it i.e. the Liquidator "steals" funds from the user

If confirmed this would be a high severity finding.

deluca-mike commented 2 years ago

Yes, there is a specific reason. Unlike OpenZeppelin contracts, which are relatively highly opinionated (and thus can carry bloat and inflexibility), ours are intended to be used by "advanced devs", and thus give them more flexibility, which includes the flexibility to shoot yourself in the foot.

Reasons the library does not revert:

Now, specifically in liquidatePortion, we intentionally don't revert since the only thing that the liquidation contact cares is that the require line's condition is met. If a transfer of tokens to the caller fails, but the contract still gains funds it expects, then that's all the contract cares about.

So, again, we'd be ok with the Liquidator "stealing" funds from the user, because the Liquidator only cares that it is satisfied.

Now, as I am thinking about it, it might make callers feel more at ease to call liquidatePortion if the check was done, since the contract would check for them that collateralAsset was properly sent to them. I'd argue that that check should happen on their end, in the msg.sender.call(data_);, but then the argument can be made that EOAs don't have this luxury. So, in light of that, I think it does make sense to wrap and revert on that transfer.

lucas-manuel commented 2 years ago

Yes this decision was originally intentional, as the strategy contracts that we use (UniswapV2, Sushiswap) would revert on the swap. However people that are using it as an EOA (OTC-style) would definitely prefer to have the require, so I think we should add it as well.

Made a ticket to track this issue.

lucas-manuel commented 2 years ago

Also made a ticket to add a comment in the erc20-helper README to justify our reasoning about the non-reversion.

lucas-manuel commented 2 years ago

Opened a PR for the require on transfer in liquidationPortion, not sure if you want to make a dedicated issue for that though.

snd commented 2 years ago

I think making a dedicated issue for it makes sense. Feel free to do so or let us know if you prefer us to do so.

lucas-manuel commented 2 years ago

Yeah for sure, you guys can make an issue for this if you'd like, there are currently two tickets so maybe two issues:

lucas-manuel commented 2 years ago

Made a PR in erc20-helper, can repost both of the PRs in the new issues once they're made

snd commented 2 years ago

Opened two issues as they will appear in the report:

https://github.com/maple-labs/trail-of-bits-audit/issues/15 https://github.com/maple-labs/trail-of-bits-audit/issues/16

The writeup is still in progress.