Velodrome Finance contest - Ruhum's results

A base layer AMM on Optimism, inspired by Solidly.

General Information

Platform: Code4rena

Start Date: 23/05/2022

Pot Size: $75,000 USDC

Total HM: 23

Participants: 75

Period: 7 days

Judge: GalloDaSballo

Total Solo HM: 13

Id: 130

League: ETH

Velodrome Finance

Findings Distribution

Researcher Performance

Rank: 2/75

Findings: 4

Award: $9,014.68

🌟 Selected for report: 2

πŸš€ Solo Findings: 2

Findings Information

🌟 Selected for report: Ruhum

Labels

bug
3 (High Risk)
disagree with severity

Awards

6473.16 USDC - $6,473.16

External Links

Lines of code

https://github.com/code-423n4/2022-05-velodrome/blob/main/contracts/contracts/redeem/RedemptionReceiver.sol#L72-L105

Vulnerability details

Impact

According to the LayerZero docs, the default behavior is that when a transaction on the destination application fails, the channel between the src and dst app is blocked. Before any new transactions can be executed, the failed transaction has to be retried until it succeeds.

See https://layerzero.gitbook.io/docs/faq/messaging-properties#message-ordering & https://layerzero.gitbook.io/docs/guides/advanced/nonblockinglzapp

So an attacker is able to initiate a transaction they know will fail to block the channel between FTM and Optimism. The RedemptionSender & Receiver won't be usable anymore.

Proof of Concept

The RedemptionReceiver contract doesn't implement the non-blocking approach as seen here: https://github.com/code-423n4/2022-05-velodrome/blob/main/contracts/contracts/redeem/RedemptionReceiver.sol#L72-L105

An example implementation of the non-blocking approach by LayerZero: https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/lzApp/NonblockingLzApp.sol

Tools Used

none

Use the non-blocking approach as described here

#0 - pooltypes

2022-06-13T18:01:35Z

Duplicate of #87

#1 - GalloDaSballo

2022-06-28T23:07:49Z

@pooltypes Can anyone send a message or would they need to be whitelisted?

#2 - GalloDaSballo

2022-07-01T00:54:57Z

If anyone can call and deny, the contract is not suited to handle exceptions and doesn't implement the forceReceive function, meaning the channel can be griefed and I don't believe there's a way to remedy.

The contract needs to implement forceResumeReceive to allow to remove malicious messages that may be received

I still am unsure if anyone can send a malicious message or if they need to be approved

#3 - GalloDaSballo

2022-07-01T00:55:21Z

If only the admin can this is a Medium Severity, if anyone can, this is a High Severity finding

#4 - GalloDaSballo

2022-07-01T00:56:21Z

From the documentation it seems like anyone can call the function: https://layerzero.gitbook.io/docs/guides/master/how-to-send-a-message

#5 - GalloDaSballo

2022-07-01T23:53:48Z

With the information that I have it seems like anyone can grief the endpoint making claims revert indefinitely, have reached out to the sponsor as well as LayerZero but have yet to receive any reply

#6 - GalloDaSballo

2022-07-05T19:44:25Z

With the information I currently have, it seems like the channel can be setup to receive messages only by the specified contract, however for multiple reasons, the message sent can cause a revert, and in lack of a "nonblocking" architecture, the messages can get stuck indefinitely.

However, the implementation under scope has none of these defenses, it seems like the contact under scope can be denied functionality by any caller that builds their own LZApp

See example of how to prevent untrusted callers: https://github.com/LayerZero-Labs/solidity-examples/blob/e46a95ce93347aa65680bef288e206af0e5a8917/contracts/lzApp/LzApp.sol#L28

Because of that, the message queue can be filled with blocking messages that cannot be removed.

Because the contract under scope also has no way of re-setting the queue, I have reason to believe that any attack can permanently brick the receiver.

For these reasons, I believe High Severity to be more appropriate

#7 - ethzoomer

2022-07-07T21:31:25Z

At this point in time we've already completed all of the redemptions

Is it possible to send a message from the contract other than what sender sends? lz's msg queues are per src addr. https://layerzero.gitbook.io/docs/faq/messaging-properties "STORED message will block the delivery of any future message from srcUA to all dstUA on the same destination chain and can be retried until the message becomes SUCCESS" The only way that can get gummed up is if redemption's over right?

#8 - GalloDaSballo

2022-07-07T22:23:53Z

My understanding is any sender can block the queue as the receiver will revert

That said if redemption is over, there's no loss beside the risk of burning funds from the FTM side

Findings Information

🌟 Selected for report: 0xf15ers

Also found by: 0x52, Ruhum, WatchPug, berndartmueller, cccz, horsefacts, hyh, minhquanym, pauliax

Labels

bug
duplicate
2 (Med Risk)

Awards

75.235 USDC - $75.24

External Links

Lines of code

https://github.com/code-423n4/2022-05-velodrome/blob/main/contracts/contracts/Bribe.sol#L41-L60

Vulnerability details

Impact

An attacker can add worthless reward tokens to reach the max possible number of tokens at no cost to the Bribe contract. They deny any legit tokens from being added.

Proof of Concept

Attacker calls Bribe.notifyRewardAmount() repeatedly a worthless custom token.

  function notifyRewardAmount(address token, uint amount) external lock {
      require(amount > 0);
      if (!isReward[token]) {
        require(rewards.length < MAX_REWARD_TOKENS, "too many rewards tokens");
      }
      // bribes kick in at the start of next bribe period
      uint adjustedTstamp = getEpochStart(block.timestamp);
      uint epochRewards = tokenRewardsPerEpoch[token][adjustedTstamp];

      _safeTransferFrom(token, msg.sender, address(this), amount);
      tokenRewardsPerEpoch[token][adjustedTstamp] = epochRewards + amount;

      if (!isReward[token]) {
          isReward[token] = true;
          rewards.push(token);
          IGauge(gauge).addBribeRewardToken(token);
      }

      emit NotifyReward(msg.sender, token, adjustedTstamp, amount);
  }

After calling the function 16 times, subsequent calls will fail because of the check on L44:

      if (!isReward[token]) {
        require(rewards.length < MAX_REWARD_TOKENS, "too many rewards tokens");
      }

If the user would use a legit token, that require statement wouldn't be called.

Tools Used

none

There are multiple ways to fix this.

  1. Use a whitelist to only allow specific tokens to be used
  2. Make the function permissioned
  3. Use a system where you're not forced to loop over the rewards array. Then you don't have to limit the number of tokens.

#0 - pooltypes

2022-06-13T15:52:31Z

Duplicate of #182

#1 - GalloDaSballo

2022-06-28T22:48:39Z

Dup of #182

Findings Information

🌟 Selected for report: IllIllI

Also found by: Ruhum, rotcivegaf

Labels

bug
duplicate
2 (Med Risk)

Awards

524.326 USDC - $524.33

External Links

Report

Low

L-01: VotingEscrow.safeTransferFrom is not ERC721 complaint

After calling IERC721Receiver(_to).onERC721Received() you have to verify that the returned value is bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")).

Relevant code:

Non-Critical

N-01: emit an event when making changes to a contract's configuration

Whenever you make changes to a contract's config it should emit an event.

Relevant code:

./contracts/Voter.sol:82: function setGovernor(address _governor) public { ./contracts/Voter.sol:87: function setEmergencyCouncil(address _council) public { ./contracts/Velo.sol:26: function setMinter(address _minter) external { ./contracts/Velo.sol:31: function setRedemptionReceiver(address _receiver) external { ./contracts/Gauge.sol:188: function setVoteStatus(address account, bool voted) external { ./contracts/Bribe.sol:30: function setGauge(address _gauge) external { ./contracts/RewardsDistributor.sol:318: function setDepositor(address _depositor) external { ./contracts/VeloGovernor.sol:39: function setTeam(address newTeam) external { ./contracts/VeloGovernor.sol:44: function setProposalNumerator(uint256 numerator) external { ./contracts/Minter.sol:64: function setTeam(address _team) external { ./contracts/Minter.sol:75: function setTeamRate(uint _teamRate) external { ./contracts/VotingEscrow.sol:1060: function setVoter(address _voter) external {

#0 - GalloDaSballo

2022-07-04T22:08:54Z

Marking this as Dup of #185 raising to Med and deleting this from QA

Findings Information

🌟 Selected for report: Ruhum

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

1941.948 USDC - $1,941.95

External Links

Lines of code

https://github.com/code-423n4/2022-05-velodrome/blob/main/contracts/contracts/redeem/RedemptionSender.sol#L43

Vulnerability details

Impact

When sending a msg to the layer zero endpoint you include enough gas for the transaction. If you don't include enough tokens for the gas, the transaction will fail. The RedemptionSender contract allows the user to pass any value they want which might result in them sending not enough. Their transaction will fail.

To know how much you have to send there's the estimateFees() function as described here

Proof of Concept

    function redeemWEVE(
        uint256 amount,
        address zroPaymentAddress,
        bytes memory zroTransactionParams
    ) public payable {
        require(amount != 0, "AMOUNT_ZERO");
        require(
            IERC20(weve).transferFrom(
                msg.sender,
                0x000000000000000000000000000000000000dEaD,
                amount
            ),
            "WEVE: TRANSFER_FAILED"
        );

        ILayerZeroEndpoint(endpoint).send{value: msg.value}(
            optimismChainId,
            abi.encodePacked(optimismReceiver),
            abi.encode(msg.sender, amount),
            payable(msg.sender),
            zroPaymentAddress,
            zroTransactionParams
        );
    }

Tools Used

none

Use the estimateFees() endpoint

#0 - GalloDaSballo

2022-06-28T23:01:09Z

The transaction shown has a case in which it could succeed but the message wouldn't be forwarded.

That is because any estimate_gas will make the tx work but the gas payment may not be sufficient to fund the message fees.

Because this can cause burned tokens, I believe a check should be added and since the tx must originate from the fantomSender there would be no way of forwarding the message on the behalf of the sender.

For those reasons I believe the finding is of Medium Severity

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