Badger Citadel contest - pauliax's results

Bringing BTC to DeFi

General Information

Platform: Code4rena

Start Date: 04/02/2022

Pot Size: $30,000 USDC

Total HM: 3

Participants: 37

Period: 3 days

Judge: leastwood

Id: 84

League: ETH

BadgerDAO

Findings Distribution

Researcher Performance

Rank: 4/37

Findings: 4

Award: $1,938.19

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: WatchPug

Also found by: 0x1f8b, Czar102, cccz, cmichel, gellej, harleythedog, hickuphh3, hyh, pauliax, sirhashalot

Labels

bug
duplicate
2 (Med Risk)

Awards

625.1882 USDC - $625.19

External Links

Lines of code

https://github.com/code-423n4/2022-02-badger-citadel/blob/main/contracts/TokenSaleUpgradeable.sol#L258

Vulnerability details

Impact

I thought of a potential rug pull from the owner: when users buy tokenOut, it is not required that the contract has already escrowed enough tokenOut. It is only required when finalizing the sale:

  require(
      tokenOut.balanceOf(address(this)) >= totalTokenOutBought,
      "TokenSale: not enough balance"
  );

This doesn't look fair from the perspective of users. Users have to trust that after the sale the owner will send enough tokens and finalize the sale. This increases the trust of one entity, while the goal of smart contracts should be to minimize this. When designing a smart contract, it should aim for as fair model as possible. Now the owner of the contract is privileged, the saleRecipient instantly receives tokenIn, but users will have to wait till the owner finalizes the sale.

I thought if this should be submitted as of low severity, but because this is explicitly mentioned in the README: "Specific care should be put in: Rug Vectors" I decided to submit this as of medium severity.

Suggestions:

  1. Escrow tokenOut upfront, verify contract holds enough when a user buys the token.
  2. Allow anyone to finalize the sale once the sale ends, or even replace this function with a view that returns true when the sale has ended.
  3. After the sale, an owner can sweep unsold tokenOut.

#0 - GalloDaSballo

2022-02-10T01:49:52Z

I agree with the finding, instead of transferring to the multisig we should use the sale contract as escrow, we should also add a ragequit function that allows depositors to get their initial deposit back if the owner doesn't finalize within X time

#1 - 0xleastwood

2022-03-14T12:07:21Z

Duplicate of #50

#2 - 0xleastwood

2022-03-16T12:27:14Z

Duplicate of #61

Findings Information

🌟 Selected for report: Czar102

Also found by: TomFrenchBlockchain, cmichel, gellej, gzeon, pauliax, pedroais, tqts

Labels

bug
duplicate
2 (Med Risk)

Awards

1179.1958 USDC - $1,179.20

External Links

Lines of code

https://github.com/code-423n4/2022-02-badger-citadel/blob/main/contracts/TokenSaleUpgradeable.sol#L303-L309 https://github.com/code-423n4/2022-02-badger-citadel/blob/main/contracts/TokenSaleUpgradeable.sol#L223

Vulnerability details

Impact

Users should be able to control the accepted price. The owner can anytime invoke function setTokenOutPrice and thus change the ratio of token in/out. Users have to trust the owner not to front-run them and make the tokens more expensive.

It would be fair if users can specify the (min) price, or (min) token out the amount that they are happy with when calling the buy function, so in case it is updated before the tx is mined, it will not execute the buy.

#0 - GalloDaSballo

2022-02-10T01:48:19Z

I think the issue of owner privilege has been mentioned by other wardens, and this may end up being used as a duplicate. I think this solution of adding a slippage check on buy is welcomed and we should probably add it (or make the price immutable)

#1 - 0xleastwood

2022-03-14T12:08:10Z

Duplicate of #50 but warden already has an issue that is being considered under this.

#2 - 0xleastwood

2022-03-16T13:10:28Z

Duplicate of #105

Findings Information

Awards

87.9099 USDC - $87.91

Labels

bug
QA (Quality Assurance)

External Links

  • Open todos in the codebase, e.g.:
 TODO: Better revert strings
  • Consider introducing a claim deadline. after the deadline, unclaimed tokens can be swept or burned by the owner. This would prevent tokens from being stuck forever in the contract in case a user is unavailable to claim them.

  • Consider introducing reasonable upper limits for saleStart and saleDuration. This would further reduce the trust in the owner, who can grief by e.g. calling setSaleStart very far in the future, or setSaleDuration too long for our generation to finish, thus making users never receive their tokens. Think of reasonable values, for instance, saleStart max 1 month in the future, and when updating validate it against the initial value, and saleDuration max 1 year, or something like that. Or these values could be configurable on initialization but should be transparent (users can validate).

  • The comment says that it pauses the sale:

  * @notice Pause the sale. Can only be called by owner
  function pause() external onlyOwner

But whenNotPaused modifier is also applied to the function claim which is invokable after the sale has been finalized. This again increases the trust of the owner, who can pause the claims leaving users no choice to claim their tokens. However, I also understand that from your point of view, this would leave no option to stop claims in case of an emergency situation. So submitting this FYI to decide if changes are necessary here.

  • function setTokenOutPrice should also validate !finalized. While this does not cause any harm, it makes it more coherent if the price can't be updated once the sale is finalized.

  • function buy can backrun initialize when saleStart = block.timestamp:

  require(saleStart <= block.timestamp, "TokenSale: not started");

In my opinion, it would be better to require _saleStart > block.timestamp in initialize function so that all the buys can't come in the same block. But I do not see any direct harm of this, so it is just a suggestion.

  • It does not validate that a _daoId exists, and the user choice can't be changed even in the case of a non-existent id. Or if the id is not specified, it will be favoring a default of 0 id dao. I see 2 possible improvements here:
  1. Allow the users to change daoVotedFor for all their boughtAmounts. Maybe apply other restrictions, e.g. can be changed until the sale is finalized.
  2. Introduce a dao registry, where valid ids of DAOs could be stored.

#0 - GalloDaSballo

2022-02-10T01:46:00Z

Appreciate the findings

Findings Information

Awards

45.891 USDC - $45.89

Labels

bug
G (Gas Optimization)

External Links

  • This can be cached to avoid external call and re-calcultation every time unless it was intended in case the token decimals might change:
  10**tokenOut.decimals()
  • 'unchecked' directive can be applied to math operations where overflow/underflow can't happen:
    if (totalTokenIn < tokenInLimit) {
        limitLeft_ = tokenInLimit - totalTokenIn;
    }
  • Would avoid duplicate calculation if you replace this:
      require(
          totalTokenIn + _tokenInAmount <= tokenInLimit,
          "total amount exceeded"
      );
     ...
     totalTokenIn += _tokenInAmount;

with:

    totalTokenIn += _tokenInAmount; 
    require(
        totalTokenIn <= tokenInLimit,
        "total amount exceeded"
    );
   ...
  • saleStart state variable is read twice here, better cache it to a local variable and re-use:
  require(saleStart <= block.timestamp, "TokenSale: not started");
  require(
      block.timestamp < saleStart + saleDuration,
      "TokenSale: already ended"
  );
AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter