Lybra Finance - MohammedRizwan's results

A protocol building the first interest-bearing omnichain stablecoin backed by LSD.

General Information

Platform: Code4rena

Start Date: 23/06/2023

Pot Size: $60,500 USDC

Total HM: 31

Participants: 132

Period: 10 days

Judge: 0xean

Total Solo HM: 10

Id: 254

League: ETH

Lybra Finance

Findings Distribution

Researcher Performance

Rank: 51/132

Findings: 2

Award: $155.96

Gas:
grade-b

🌟 Selected for report: 0

šŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: Neon2835

Also found by: 0xRobocop, 0xcm, Arz, DedOhWale, HE1M, MohammedRizwan, azhar, kankodu, zaevlad

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-769

Awards

143.4901 USDC - $143.49

External Links

Lines of code

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/PeUSDMainnetStableVision.sol#L26 https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/PeUSDMainnetStableVision.sol#L132

Vulnerability details

Impact

In PeUSDMainnetStableVision.sol executeFlashloan().

File: contracts/lybra/token/PeUSDMainnetStableVision.sol

129    function executeFlashloan(FlashBorrower receiver, uint256 eusdAmount, bytes calldata data) public payable {
130        uint256 shareAmount = EUSD.getSharesByMintedEUSD(eusdAmount);
131        EUSD.transferShares(address(receiver), shareAmount);
132        receiver.onFlashLoan(shareAmount, data);
133        bool success = EUSD.transferFrom(address(receiver), address(this), EUSD.getMintedEUSDByShares(shareAmount));
134        require(success, "TF");
135
136        uint256 burnShare = getFee(shareAmount);
137        EUSD.burnShares(address(receiver), burnShare);
138        emit Flashloaned(receiver, eusdAmount, burnShare);
139    }

In PeUSDMainnetStableVision.sol executeFlashloan() function doesn't pass the initiator. That makes it more difficult to integrate the executeFlashloan functionality.

As per eip-3156,

A initiator will often be required in the onFlashLoan function, which the lender knows as msg.sender. An alternative implementation which would embed the initiator in the data parameter by the caller would require an additional mechanism for the receiver to verify its accuracy, and is not advisable.

As per eip-3156,

Any receiver that keeps an approval for a given lender needs to include in onFlashLoan a mechanism to verify that the initiator is trusted.

Proof of Concept

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/PeUSDMainnetStableVision.sol#L26

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/PeUSDMainnetStableVision.sol#L132

As per eip-3156. This is reference implementation in which initiator is authenticated.


    /// @dev ERC-3156 Flash loan callback
    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external override returns(bytes32) {
        require(
            msg.sender == address(lender),
            "FlashBorrower: Untrusted lender"
        );
        require(
            initiator == address(this),
            "FlashBorrower: Untrusted loan initiator"
        );
        (Action action) = abi.decode(data, (Action));
        if (action == Action.NORMAL) {
            // do one thing
        } else if (action == Action.OTHER) {
            // do another
        }
        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }

Refer eip-3156 for reference.

Tools Used

Manual review

Add the initiator authentication and follow eip-3156.

Assessed type

Other

#0 - c4-pre-sort

2023-07-04T14:00:33Z

JeffCX marked the issue as duplicate of #280

#1 - c4-judge

2023-07-28T15:29:32Z

0xean marked the issue as satisfactory

#2 - c4-judge

2023-07-28T19:53:22Z

0xean changed the severity to 3 (High Risk)

Awards

12.4743 USDC - $12.47

Labels

bug
G (Gas Optimization)
grade-b
sponsor acknowledged
G-04

External Links

Summary

Gas Optimizations

IssueInstances
[G‑01]Save gas by preventing zero amount in mint() and burn()2
[G‑02]Catch function parameters as local variables to save gas1
[G‑03]Use nested-if and avoid "&&" to save gas2
[G‑04]Catch proposalId in LybraGovernance.sol within function3

[G‑01] Save gas by preventing zero amount in mint() and burn()

In esLBR.sol, L-30 to L-43. There are 2 instances of this issue: Link to code

File: contracts/lybra/token/esLBR.sol

    function mint(address user, uint256 amount) external returns (bool) {
+       require(amount != 0, "invalid amount);
        require(configurator.tokenMiner(msg.sender), "not authorized");
        require(totalSupply() + amount <= maxSupply, "exceeding the maximum supply quantity.");
        try IProtocolRewardsPool(configurator.getProtocolRewardsPool()).refreshReward(user) {} catch {}
        _mint(user, amount);
        return true;
    }

    function burn(address user, uint256 amount) external returns (bool) {
+       require(amount != 0, "invalid amount);
        require(configurator.tokenMiner(msg.sender), "not authorized");
        try IProtocolRewardsPool(configurator.getProtocolRewardsPool()).refreshReward(user) {} catch {}
        _burn(user, amount);
        return true;
    }

[G‑02] Catch function parameters as local variables to save gas

Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read.

In esLBRBoost.sol, L-57 to L-69 There is 1 instance of this issue: Link to code

File: contracts/lybra/miner/esLBRBoost.sol

    function getUserBoost(address user, uint256 userUpdatedAt, uint256 finishAt) external view returns (uint256) {
+       address _user = user;
+       uint256 _userUpdatedAt = userUpdatedAt;
+       uint256 _finishAt = finishAt;
-        uint256 boostEndTime = userLockStatus[user].unlockTime;
+        uint256 boostEndTime = userLockStatus[_user].unlockTime;
-        uint256 maxBoost = userLockStatus[user].miningBoost;
+        uint256 maxBoost = userLockStatus[_user].miningBoost;

-        if (userUpdatedAt >= boostEndTime || userUpdatedAt >= finishAt) {
+        if (_userUpdatedAt >= boostEndTime || _userUpdatedAt >= _finishAt) {
            return 0;
        }
-        if (finishAt <= boostEndTime || block.timestamp <= boostEndTime) {
+        if (_finishAt <= boostEndTime || block.timestamp <= boostEndTime) {
            return maxBoost;
        } else {
-            uint256 time = block.timestamp > finishAt ? finishAt : block.timestamp;
+            uint256 time = block.timestamp > _finishAt ? _finishAt : block.timestamp;
-            return ((boostEndTime - userUpdatedAt) * maxBoost) / (time - userUpdatedAt);
+            return ((boostEndTime - _userUpdatedAt) * maxBoost) / (time - _userUpdatedAt);
        }
    }

[G‑03] Use nested-if else and avoid "&&" to save gas

In LBR.sol at L-64, There is 1 instance of this issue: Link to code

[G‑04] Catch proposalId in LybraGovernance.sol within function

Catch the proposalId as a local variable in _quorumReached(), _voteSucceeded(), _countVote() to save Gwarmaccess (100 gas). In LybraGovernance.sol at L-59 to L-76. There is 2 instance of this issue: Link to code

In PeUSDMainnetStableVision.sol at L-199 Link to code

#0 - c4-sponsor

2023-07-27T06:58:50Z

LybraFinance marked the issue as sponsor acknowledged

#1 - c4-judge

2023-07-27T23:41:55Z

0xean 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