Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AmmGauge stakeFor/unstakeFor allow for reentrancy that can lead to stealing the whole contract balance #62

Open
code423n4 opened this issue Jun 3, 2022 · 2 comments
Labels
bug Something isn't working QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax sponsor disputed Sponsor cannot duplicate the issue, or otherwise disagrees this is an issue

Comments

@code423n4
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/AmmGauge.sol#L108-L113
https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/AmmGauge.sol#L130-L135

Vulnerability details

Some ERC20 do allow for user's control of transfer call execution. For example, ERC777 has tokensReceived() hook. This way, the execution flow can be reentered with the usage of any such tokens. This possibility can be a part of current implementation or can be introduced in the future with token contract upgrade. AmmGauge deals with an arbitrary AMM LP tokens, which can have such control now or be upgraded to have it in the future for various reasons.

AmmGauge's stakeFor() and unstakeFor() do not control for reentrancy and interact with AMM token contract before performing the accounting update. Balance difference is used to calculate how much to add to user's accounted funds. Together this allows for the following reentrancy based attack surface.

Suppose Bob has 100 tokens, runs stakeFor() and reenters 4 times, so run stakeFor() 5 times in total, with 20 token each time. His first run will recognize 100 tokens as a deposit, the second run will recognize 80, and only his last one will recognize only 20 tokens he actually sent. This way the AmmGauge records 100 + 80 + 60 + 40 + 20 = 300 tokens for the 20 * 5 = 100 tokens Bob actually deposited.

This way Bob will bloat his account and finally exit with all LP token funds AmmGauge have.

Setting severity to medium as that's the loss of funds of all other stakers conditional on AMM token execution flow control probability, which can be reasonably low, but isn't a zero.

Proof of Concept

stakeFor() allows reentrancy and determine the amount provided after pulling the funds from the user based on balance change:

https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/AmmGauge.sol#L108-L113

Similarly, unstakeFor() allows reentrancy and can be gamed by orchestrating the cumulative balance change:

https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/AmmGauge.sol#L130-L135

_userCheckpoint() called in the both cases will record zero Backd reward balance change on the nested runs, not preventing execution.

Recommended Mitigation Steps

Consider rearranging the operations so that external contract calls be placed after internal accounting is updated, for example:

Now (unstakeFor):

  uint256 oldBal = IERC20(ammToken).balanceOf(address(this));
  IERC20(ammToken).safeTransfer(dst, amount);
  uint256 newBal = IERC20(ammToken).balanceOf(address(this));
  uint256 unstaked = oldBal - newBal;
  balances[msg.sender] -= unstaked;
  totalStaked -= unstaked;

Rearranged (accounting first, interaction, correction):

  balances[msg.sender] -= amount;
  totalStaked -= amount;
  uint256 oldBal = IERC20(ammToken).balanceOf(address(this));
  IERC20(ammToken).safeTransfer(dst, amount);
  uint256 newBal = IERC20(ammToken).balanceOf(address(this));
  int256 unstakedDiff = (oldBal - newBal) - amount;
  if (unstakedDiff != 0) {
  		balances[msg.sender] -= unstakedDiff;
  		totalStaked -= unstakedDiff;
  		// place any additional logic for amount diff case here, if needed
  }

Also consider introducing reentrancy control, for example a nonReentrant modifier, to all user facing functions that perform fund transfers.

This also is relevant for AmmConvexGauge's stakeFor() and unstakeFor():

https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/AmmConvexGauge.sol#L157-L182

@code423n4 code423n4 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Jun 3, 2022
code423n4 added a commit that referenced this issue Jun 3, 2022
@chase-manning
Copy link
Collaborator

We do not support tokens that offer reentrancy opportunities

@chase-manning chase-manning added the sponsor disputed Sponsor cannot duplicate the issue, or otherwise disagrees this is an issue label Jun 6, 2022
@GalloDaSballo
Copy link
Collaborator

Dup of #19

@GalloDaSballo GalloDaSballo added duplicate This issue or pull request already exists QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax and removed 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value labels Jun 21, 2022
@JeeberC4 JeeberC4 removed the duplicate This issue or pull request already exists label Jun 23, 2022
@JeeberC4 JeeberC4 reopened this Jun 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax sponsor disputed Sponsor cannot duplicate the issue, or otherwise disagrees this is an issue
Projects
None yet
Development

No branches or pull requests

4 participants