Asymmetry Finance afETH Invitational - rvierdiiev's results

The aggregation and optimization layer for Liquid Staking Tokens.

General Information

Platform: Code4rena

Start Date: 18/09/2023

Pot Size: $31,310 USDC

Total HM: 15

Participants: 5

Period: 7 days

Judge: 0xLeastwood

Total Solo HM: 6

Id: 286

League: ETH

Asymmetry Finance

Findings Distribution

Researcher Performance

Rank: 1/5

Findings: 7

Award: $0.00

QA:
grade-a
Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: adriro

Also found by: d3e4, rvierdiiev

Labels

bug
3 (High Risk)
partial-25
upgraded by judge
duplicate-36

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/AfEth.sol#L175-L215

Vulnerability details

Impact

AfEth withdrawing will not work when ratio will be 0. It will be not possible to withdraw.

Proof of Concept

Any ratio for the 2 tokens of afEth can be set by owner. AfEth.withdraw function will not work in case if ratio will be 0, which means that safEth percentage is 0. This is because, safEth doesn't allow to unstake 0 amount.

Also, when ratio is e18, that means that vEth percentage is 0, but requestWithdraw function will still calculate withdraw time, which is not needed in this case and withdraw will be called.

Tools Used

VsCode

First of all, calculate withdraw time only for vEth amount, not whole amount. Then in case if ratio is e18, then you don't need to do that. Also don't unstake/withdraw 0 shares for safEth and vEth.

Assessed type

Error

#1 - c4-judge

2023-10-03T18:30:37Z

0xleastwood marked the issue as duplicate of #36

#2 - c4-judge

2023-10-04T08:58:11Z

0xleastwood marked the issue as not a duplicate

#3 - c4-judge

2023-10-04T08:58:18Z

0xleastwood marked the issue as duplicate of #18

#4 - 0xleastwood

2023-10-04T09:00:45Z

So technically this is a duplicate of #18 and #36 but it fails to really describe either one in full detail. Not sure how this should be treated, but I will try to duplicate this issue so that it can be partially rewarded for both. 50% for #18 and 25% for #36.

#5 - c4-judge

2023-10-04T18:57:54Z

0xleastwood changed the severity to 3 (High Risk)

#6 - c4-judge

2023-10-04T18:59:24Z

0xleastwood marked the issue as partial-50

#7 - c4-judge

2023-10-04T19:15:39Z

0xleastwood changed the severity to QA (Quality Assurance)

#8 - c4-judge

2023-10-04T19:16:23Z

This previously downgraded issue has been upgraded by 0xleastwood

#9 - c4-judge

2023-10-04T19:16:40Z

0xleastwood changed the severity to QA (Quality Assurance)

#10 - c4-judge

2023-10-04T19:19:37Z

This previously downgraded issue has been upgraded by 0xleastwood

#11 - c4-judge

2023-10-04T19:19:44Z

0xleastwood marked the issue as full credit

#12 - c4-judge

2023-10-04T19:19:49Z

0xleastwood marked the issue as not a duplicate

#13 - c4-judge

2023-10-04T19:20:07Z

0xleastwood marked the issue as duplicate of #36

#14 - c4-judge

2023-10-04T19:20:21Z

0xleastwood changed the severity to 3 (High Risk)

#15 - c4-judge

2023-10-04T19:20:26Z

0xleastwood marked the issue as partial-25

Findings Information

🌟 Selected for report: adriro

Also found by: MiloTruck, d3e4, rvierdiiev

Labels

bug
3 (High Risk)
partial-50
upgraded by judge
duplicate-34

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/AfEth.sol#L138 https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategy.sol#L32

Vulnerability details

Impact

Stale cvx price can be used while depositing

Proof of Concept

When user deposits, then price of afEth token is calculated. It's needed to know how many tokens user will receieve.

This price consists of safEth price and vEth price.

This is how price is found for vEth. https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategy.sol#L31-L33

    function price() external view override returns (uint256) {
        return (cvxPerVotium() * ethPerCvx(false)) / 1e18;
    }

Here, ethPerCvx function is called and false is passed as param. This param tells function if it's needed to validate chainlink price. In case if it's false then there will be no validation and as result there is a risk that chainlink will return stale price. As user also provides slippage that actually doesn't mean that he will not be affected, it's still possible that wrong price will fit slippage.

Tools Used

VsCode

Validate chainlink data in this case.

Assessed type

Error

#1 - c4-judge

2023-10-03T18:16:57Z

0xleastwood marked the issue as duplicate of #34

#2 - c4-judge

2023-10-04T08:21:32Z

0xleastwood changed the severity to 3 (High Risk)

#3 - c4-judge

2023-10-04T08:22:37Z

0xleastwood marked the issue as partial-50

#4 - 0xleastwood

2023-10-04T08:23:00Z

Partial credit because it is lacking additional information about impact.

#5 - c4-judge

2023-10-04T08:23:12Z

0xleastwood marked the issue as satisfactory

#6 - c4-judge

2023-10-04T08:23:22Z

0xleastwood removed the grade

#7 - c4-judge

2023-10-04T08:23:44Z

0xleastwood marked the issue as partial-50

#8 - rvierdiyev

2023-10-06T08:26:23Z

@0xleastwood with all respect, but impact is described at the top

When user deposits, then price of afEth token is calculated. It's needed to know how many tokens user will receieve.

so this means that wrong price will give wrong tokens amount

#9 - 0xleastwood

2023-10-06T08:50:24Z

That's not why I gave partial credit. You failed to detail the impact of such issue and under what situations this will affect deposits. Look at the primary issue and #28 to see what it would look like to contain sufficient context. And then look at #64 which I also gave partial credit to.

#10 - rvierdiyev

2023-10-06T09:12:16Z

thanks, i will try to learn the difference and improve

Findings Information

🌟 Selected for report: MiloTruck

Also found by: adriro, d3e4, rvierdiiev

Labels

bug
3 (High Risk)
satisfactory
duplicate-25

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/AfEth.sol#L133-L141

Vulnerability details

Impact

Attacker can mint afEth with cheaper price and then withdraw.

Proof of Concept

When user would like to buy some amount of afEth tokens, then price is calculated. This price will be used to calculate amount of tokens to mint.

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/AfEth.sol#L133-L141

    function price() public view returns (uint256) {
        if (totalSupply() == 0) return 1e18;
        AbstractStrategy vEthStrategy = AbstractStrategy(vEthAddress);
        uint256 safEthValueInEth = (ISafEth(SAF_ETH_ADDRESS).approxPrice(true) *
            safEthBalanceMinusPending()) / 1e18;
        uint256 vEthValueInEth = (vEthStrategy.price() *
            vEthStrategy.balanceOf(address(this))) / 1e18;
        return ((vEthValueInEth + safEthValueInEth) * 1e18) / totalSupply();
    }

This is how price is calculated. First, function calculates eth amount that contract holds in safEth tokens. You can see, that safEthBalanceMinusPending function is used to get amount. This function doesn't include safEth that is requested to withdraw. And then vEth amount in terms of eth is calculated. And this total amount then divided by whole totalSupply.

Now, we need to look, how withdraw requests work. First important this is that afEth, sent by user, isn't burnt, but transferred to the contract, which means that totalSupply remains same after request. But, when vEth is requested to withdraw, then it shares are burnt at the moment of request. Also pendingSafEthWithdraws is increased with safEth amount.

This means that when withdraw request is done, then eth amount of safEth token and vEth token has decreased, but totalSupply of afEth is still the same. As result, price of token decreases.

While this incorrect for everyone, this also can be used by attacker(some eth whale) in order to mint tokens cheaper. To do that he will need to deposit big amount of eth to the afEth. Then signal withdraw request for all his tokens and mint for example same amount. Soon, he will start earn profit every round he does so, as he will always mint tokens cheaper.

Tools Used

VsCode

Change last line of price function: return ((vEthValueInEth + safEthValueInEth) * 1e18) / (totalSupply() - balanceOf(address(this)));

Assessed type

Error

#1 - c4-judge

2023-10-03T13:06:38Z

0xleastwood marked the issue as duplicate of #59

#2 - c4-judge

2023-10-03T13:09:44Z

0xleastwood marked the issue as duplicate of #25

#3 - c4-judge

2023-10-04T10:05:24Z

0xleastwood marked the issue as satisfactory

Findings Information

🌟 Selected for report: MiloTruck

Also found by: MiloTruck, adriro, d3e4, m_Rassska, rvierdiiev

Labels

bug
3 (High Risk)
partial-25
duplicate-23

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategyCore.sol#L292-L294

Vulnerability details

Impact

VotiumStrategyCore.applyRewards can be sandwhiched, so users rewards will be lost.

Proof of Concept

VotiumStrategyCore.applyRewards will be used in order to swap all rewards to eth and then distribute eth to the safEth or vEth.

The problem here is that sellCvx function doesn't have slippage protection, which means that attacker can sandwhich this function call and make contract lose funds during the swap. Pls, note, that AfEth.depositRewards also can't protect form this.

Tools Used

VsCode

Deal with slippage when you call sellCvx function.

Assessed type

Error

#0 - c4-judge

2023-10-03T12:38:52Z

0xleastwood marked the issue as duplicate of #39

#1 - c4-judge

2023-10-04T07:49:31Z

0xleastwood marked the issue as duplicate of #23

#2 - c4-judge

2023-10-04T07:53:32Z

0xleastwood marked the issue as partial-25

#3 - 0xleastwood

2023-10-04T07:53:51Z

Only covers 1/3 of the edge cases outlined in the primary issue.

Findings Information

🌟 Selected for report: adriro

Also found by: MiloTruck, d3e4, m_Rassska, rvierdiiev

Labels

bug
2 (Med Risk)
satisfactory
edited-by-warden
duplicate-49

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategy.sol#L184-L193

Vulnerability details

Impact

VotiumStrategy.withdrawTime doesn't expect that balance can be already unlocked. As result user can wait more time to witdraw.

Proof of Concept

When user wants to withdraw, then he needs to initiate requestWithdraw. As some part of funds are locked as cvx token inside vlcvx that means that they should be withdrawn. When you lock cvx then amount is locked for some period, so you can't withdraw it for that time.

For such reason withdrawTime function exists, which purpose is to calculate time when needed amount will be available to withdraw.

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategy.sol#L170-L195

    function withdrawTime(
        uint256 _amount
    ) external view virtual override returns (uint256) {
        uint256 _priceInCvx = cvxPerVotium();
        (
            ,
            uint256 unlockable,
            ,
            ILockedCvx.LockedBalance[] memory lockedBalances
        ) = ILockedCvx(VLCVX_ADDRESS).lockedBalances(address(this));
        uint256 cvxAmount = (_amount * _priceInCvx) / 1e18;
        uint256 totalLockedBalancePlusUnlockable = unlockable +
            IERC20(CVX_ADDRESS).balanceOf(address(this));


        for (uint256 i = 0; i < lockedBalances.length; i++) {
            totalLockedBalancePlusUnlockable += lockedBalances[i].amount;
            // we found the epoch at which there is enough to unlock this position
            if (
                totalLockedBalancePlusUnlockable >=
                cvxUnlockObligations + cvxAmount
            ) {
                return lockedBalances[i].unlockTime;
            }
        }
        revert InvalidLockedAmount();
    }

unlockable is amount that can be withdrawn right now from VLCVX_ADDRESS. And totalLockedBalancePlusUnlockable is total available balance at the moment.

As you can see, this code doesn't check that totalLockedBalancePlusUnlockable > amount, it checks it only inside the loop. And because of that, it's possible that balance is already enough, but function will use time of first lockedBalances[0].unlockTime as withdraw time, which will make user wait more to withdraw.

Exactly same thing exists in the requestWithdraw function as it actually does almost same and tries to find epoch, when funds will be available.

Tools Used

VsCode

Consider possibility, that totalLockedBalancePlusUnlockable > amount even without additional withdraw.

Assessed type

Error

#0 - c4-judge

2023-10-03T18:35:20Z

0xleastwood marked the issue as primary issue

#1 - c4-judge

2023-10-03T18:37:43Z

0xleastwood marked the issue as duplicate of #63

#2 - c4-judge

2023-10-04T10:07:09Z

0xleastwood marked the issue as satisfactory

Findings Information

🌟 Selected for report: adriro

Also found by: MiloTruck, d3e4, m_Rassska, rvierdiiev

Labels

bug
2 (Med Risk)
satisfactory
edited-by-warden
duplicate-49

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategy.sol#L170-L195

Vulnerability details

Impact

Last stakers may not receive funds back.

Proof of Concept

When user wants to withdraw, then he needs to initiate requestWithdraw. As some part of funds are locked as cvx token inside vlcvx that means that they should be withdrawn. When you lock cvx then amount is locked for some period, so you can't withdraw it for that time.

For such reason withdrawTime function exists, which purpose is to calculate time when needed amount will be available to withdraw.

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategy.sol#L170-L195

    function withdrawTime(
        uint256 _amount
    ) external view virtual override returns (uint256) {
        uint256 _priceInCvx = cvxPerVotium();
        (
            ,
            uint256 unlockable,
            ,
            ILockedCvx.LockedBalance[] memory lockedBalances
        ) = ILockedCvx(VLCVX_ADDRESS).lockedBalances(address(this));
        uint256 cvxAmount = (_amount * _priceInCvx) / 1e18;
        uint256 totalLockedBalancePlusUnlockable = unlockable +
            IERC20(CVX_ADDRESS).balanceOf(address(this));


        for (uint256 i = 0; i < lockedBalances.length; i++) {
            totalLockedBalancePlusUnlockable += lockedBalances[i].amount;
            // we found the epoch at which there is enough to unlock this position
            if (
                totalLockedBalancePlusUnlockable >=
                cvxUnlockObligations + cvxAmount
            ) {
                return lockedBalances[i].unlockTime;
            }
        }
        revert InvalidLockedAmount();
    }

This function reverts in case if it didn't trigger return in the loop. So protocol developers doesn't expect that it's possible.

However, it is. In case if there are no items in the lockedBalances, but unlockable != 0, that means that all locks are expired and such situation is likely to occur when users are going to quit AfEth token, so there is small amount of funds locked and new locks are not created.

So imagine situation that unlockable is 10 eth and user would like to withdraw it, but because loop will be skipped, then function will revert, which means that user can't withdraw. It will be still possible to recover funds by relocking, but user will have to wait more.

Same problem exists in the VotiumStrategy.requestWithdraw function.

Tools Used

VsCode

As i recommended in another finding you need to expect that totalLockedBalancePlusUnlockable at the beginning is already enough to fulfill request.

Assessed type

Error

#0 - c4-judge

2023-10-03T18:39:28Z

0xleastwood marked the issue as duplicate of #63

#1 - c4-judge

2023-10-04T10:07:18Z

0xleastwood marked the issue as satisfactory

Findings Information

🌟 Selected for report: adriro

Also found by: rvierdiiev

Labels

bug
2 (Med Risk)
satisfactory
duplicate-45

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategyCore.sol#L272-L305

Vulnerability details

Impact

VotiumStrategyCore.applyRewards can be sandwhiched to make profit.

Proof of Concept

VotiumStrategyCore.applyRewards function will swap all rewards of contract into eth and then stake them into safEth or vEth contract. As result price of afEth token will increase.

Usually, when user want to withdraw, then he needs to make withdraw request, which will calculate when it will be possible to withdraw. So on that timestamp user will then call withdraw. So this is usually 2 step process that can't be done in same tx.

But it's also possible that on the moment of VotiumStrategyCore.applyRewards function call, unlockable amount will have some gap, that user can use to withdraw instantly.

In this case user can just deposit that gap amount right before, VotiumStrategyCore.applyRewards call and then request withdraw and withdraw, all in same block. This will be easy profit for use, but condition should satisfy: unlockable should have enough gap, so user can earn at least more than tx cost on this attack.

Tools Used

VsCode

I don't know good solution.

Assessed type

Error

#0 - elmutt

2023-09-28T01:45:30Z

super interesting find. Will discuss with the team the implications of this

#1 - elmutt

2023-09-28T19:14:59Z

@rvierdiyev I think I understand the exploit but want to be sure. Here is an example how I see it:

  1. unlockable = 999 cvx
  2. exploiter sees applyRewards() being called in the mempool.
  3. exploiter front runs and calls deposit() first in such a way that withdraw() is expected to withdraw 999 cvx after applyReward() has been called.
  4. exploiter calls withdraw() in same block and profits

Am I understanding the problem correctly?

#2 - rvierdiyev

2023-09-29T07:26:11Z

@elmutt yes, but just unlockable = 999 cvx is not enough, as we have such thing as cvxUnlockObligations as well(amount that is already reserved). let's say cvxUnlockObligations is 499, so by gap i mean unlockable - cvxUnlockObligations (as new withdrawer can't pretend on obligations to other people), which is 500 in our case everything else is described correctly

#3 - c4-judge

2023-10-03T18:32:55Z

0xleastwood marked the issue as primary issue

#4 - c4-judge

2023-10-03T18:33:27Z

0xleastwood marked the issue as duplicate of #45

#5 - c4-judge

2023-10-04T09:45:47Z

0xleastwood marked the issue as satisfactory

Findings Information

🌟 Selected for report: adriro

Also found by: MiloTruck, d3e4, m_Rassska, rvierdiiev

Labels

bug
grade-a
QA (Quality Assurance)
sponsor confirmed
edited-by-warden
Q-05

Awards

Data not available

External Links

QA-01. VotiumStrategyCore.withdrawStuckTokens should not allow to withdraw cvx token.

Description

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategyCore.sol#L215-L220 VotiumStrategyCore.withdrawStuckTokens function can be used by malicious owner to withdraw cvx tokens from contract.

Recommendation

If token is cvx, then revert.

QA-02. AfEth.setRatio doesn't validate input.

Description

AfEth.setRatio doesn't validate input. Because of that it's possible that owner will provided incorrect ratio and contract will stop working.

Recommendation

Check that ratio is between 0 and e18.

#0 - 0xleastwood

2023-10-04T06:01:29Z

QA-01 is a duplicate of #53

#1 - c4-judge

2023-10-04T06:03:39Z

0xleastwood marked the issue as grade-b

#2 - c4-judge

2023-10-09T10:19:36Z

0xleastwood marked the issue as grade-a

#3 - c4-sponsor

2023-10-09T16:20:45Z

elmutt (sponsor) confirmed

Findings Information

🌟 Selected for report: adriro

Also found by: d3e4, m_Rassska, rvierdiiev

Labels

bug
G (Gas Optimization)
grade-b
edited-by-warden
G-04

Awards

Data not available

External Links

G-01.

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/AfEth.sol#L160 Calculate vValue in such way: uint256 vValue = amount - sValue; and avoid additional arithmetical operations.

G-02.

https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/AfEth.sol#L162-L164 Don't use += as totalValue is always 0. Change to:

totalValue = (sMinted * ISafEth(SAF_ETH_ADDRESS).approxPrice(true)) + (vMinted * vStrategy.price());

#0 - c4-judge

2023-10-04T05:57:13Z

0xleastwood marked the issue as grade-b

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