Abracadabra Mimswap - DarkTower's results

General Information

Platform: Code4rena

Start Date: 07/03/2024

Pot Size: $63,000 USDC

Total HM: 20

Participants: 36

Period: 5 days

Judge: cccz

Total Solo HM: 11

Id: 349

League: BLAST

Abracadabra Money

Findings Distribution

Researcher Performance

Rank: 3/36

Findings: 5

Award: $5,548.59

🌟 Selected for report: 3

🚀 Solo Findings: 2

Findings Information

🌟 Selected for report: DarkTower

Labels

bug
2 (Med Risk)
insufficient quality report
primary issue
satisfactory
selected for report
edited-by-warden
M-11

Awards

2068.8771 USDC - $2,068.88

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/oracles/aggregators/MagicLpAggregator.sol#L48-L50

Vulnerability details

Impact

MagicLpAggregator does not update the roundId, startAt, updatedAt and answeredInRound to correct values.

MagicLpAggregator.sol#L48-L50

    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {
        return (0, latestAnswer(), 0, 0, 0);
    }

A common code is to check updatedAt for staleness issue (although it isn't required to do so anymore.

(, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();

if (updatedAt < block.timestamp - 60 * 60 /* 1 hour */) {
   revert("stale price feed");
}

Therefore any integrator that uses the above code will not be able to integrate MagicLpAggregator oracles as it will always revert due to the incorrect updatedAt being provided.

Tools Used

Manual Review

Use the values roundId, startAt, updatedAt and answeredInRound from whichever oracle, baseOracle or quoteOracle was used.

Assessed type

Other

#0 - c4-pre-sort

2024-03-15T13:12:01Z

141345 marked the issue as insufficient quality report

#1 - 141345

2024-03-15T13:12:03Z

invalid won't revert

#2 - c4-judge

2024-03-29T15:13:21Z

thereksfour marked the issue as unsatisfactory: Invalid

#3 - rexjoseph

2024-04-03T11:50:25Z

Hi again @thereksfour,

The lookout's response is incorrect:

invalid won't revert

I think we should reiterate the issue here as the submission has tried its best to point out:

  1. The MagicLpAggregator gets the price from Chainlink's Feed
  2. Integrators (for example protocol A) query the MagicLPAggregator Oracle for price specifically the latestRoundData() function it exposes in their implementation
  3. They make sure the prices returned are fresh and so do well to check the time specifically updatedAt so they can be sure the feed is fresh to proceed with utilizing the returned data
  4. Since the updatedAt as well as other returned data are hardcoded to 0 in MagicLpAggregator oracle implementation, the call reverts. 0 will always be less than block.timestamp - 60 * 60

The step by step description of the issue above I believe is sufficient but here's a provided POC to elaborate on this in code:

Test file in the abracadabra codebase is: MagicLPAggregator.t.sol

function testProtocolAIntegrationOfMagicLPAggReverts() public {

        // return values of `MagicLpAggregator` latestRoundData()
        (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = aggregator.latestRoundData();

        // protocol A tries to make sure the prices returned is fresh by checking the time

        // keep in mind the time aka updatedAt is hardcoded to 0. So 0 will always be less than `block.timestamp - 60 * 60` hence a revert

        if (updatedAt < block.timestamp - 60 * 60 /* 1 hour */) {
         revert("stale price feed");
        }
    }
Ran 1 test for test/MagicLpAggregator.t.sol:MagicLpAggregatorTest
[FAIL. Reason: revert: stale price feed] testProtocolAIntegrationOfMagicLPAggReverts() (gas: 57187)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 2.56s (2.88ms CPU time)

Thank you for taking another look at this!

#4 - thereksfour

2024-04-04T15:29:14Z

latestRoundData() is different from the standard and may cause integration issues. will consider it a valid M

#5 - c4-judge

2024-04-04T15:29:20Z

thereksfour marked the issue as primary issue

#6 - c4-judge

2024-04-04T15:29:26Z

thereksfour marked the issue as satisfactory

#7 - c4-judge

2024-04-04T15:29:37Z

thereksfour marked the issue as selected for report

Findings Information

🌟 Selected for report: ether_sky

Also found by: DarkTower, SpicyMeatball, hals

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
edited-by-warden
:robot:_56_group
duplicate-90

Awards

290.0407 USDC - $290.04

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/oracles/aggregators/MagicLpAggregator.sol#L37-L46

Vulnerability details

Impact

MagicLPAggregator will return wrong price for tokens less than 18 decimals.

The MagicLPAggregator intends to return price of a LP in terms of USD in 18 decimals.

MagicLpAggregator.sol#L29-L31

 function decimals() external pure override returns (uint8) {
        return 18;
}

But let's take a look at the following function. MagicLpAggregator.sol#L37-L46

    function latestAnswer() public view override returns (int256) {
        uint256 baseAnswerNomalized = uint256(baseOracle.latestAnswer()) * (10 ** (WAD - baseOracle.decimals()));
        uint256 quoteAnswerNormalized = uint256(quoteOracle.latestAnswer()) * (10 ** (WAD - quoteOracle.decimals()));
        uint256 minAnswer = baseAnswerNomalized < quoteAnswerNormalized ? baseAnswerNomalized : quoteAnswerNormalized;

        (uint256 baseReserve, uint256 quoteReserve) = _getReserves();
        baseReserve = baseReserve * (10 ** (WAD - baseDecimals));
        quoteReserve = quoteReserve * (10 ** (WAD - quoteDecimals));
        return int256(minAnswer * (baseReserve + quoteReserve) / pair.totalSupply());
    }

Let us break down the above function, it first:

  • obtains the price returned by the baseOracle
  • obtains the price returned by the quoteOracle
  • finds the minimum price returned by the two and store in minAnswer
  • obtains the total baseReserve + quoteReserve of the MagicLP
  • takes minAnswer * (baseReserve + quoteReserve) / pair.totalSupply()
  • note: pair.totalSupply() is the total number of MagicLP shares in the pool.

But if you look at the above closely, you will notice that although the function seems correct it outputs the price in the wrong decimals (not 18):

  • We know that minAnswer will always be the normalized oracle price to 18 decimals

  • (baseReserve + quoteReserve) will always be the normalized token prices to 18 decimals

  • However, pair.totalSupply() is not normalized, and depends on the decimals of the base token!

MagicLP.sol#L163-L165

    function decimals() public view override returns (uint8) {
        return IERC20Metadata(_BASE_TOKEN_).decimals();
    }

Therefore if IERC20Metadata(_BASE_TOKEN_).decimals() is not 18 decimals, let us say 6 decimals (ie. USDT token)

Than the decimals for the final output isn't correct as 18 + 18 - 6 = 30 decimals which is 10^12 times the intended price we wanted to return.

Tools Used

Manual Review

Normalize pair.totalSupply() to 18 decimals so that the final output is 18 + 18 - 18 = 18 decimals.

Assessed type

Oracle

#0 - 0xm3rlin

2024-03-15T00:58:14Z

duplicate #191

#1 - c4-pre-sort

2024-03-15T13:01:17Z

141345 marked the issue as duplicate of #64

#2 - c4-judge

2024-03-29T15:21:38Z

thereksfour marked the issue as duplicate of #90

#3 - c4-judge

2024-03-29T16:52:43Z

thereksfour marked the issue as satisfactory

#4 - c4-judge

2024-03-29T17:04:52Z

thereksfour changed the severity to 2 (Med Risk)

Findings Information

🌟 Selected for report: DarkTower

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
edited-by-warden
:robot:_27_group
M-15

Awards

2068.8771 USDC - $2,068.88

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/blast/BlastOnboardingBoot.sol#L117

Vulnerability details

Impact

The Abracadabra team configures gas yield for all contracts in the Blast L2 except for the LockingMultiRewards contract. Therefore, gas yields accrued on the LockingMultiRewards staking contract since deployment will be lost as it was never configured using BlastYields.configureDefaultClaimables. If the contract holds native yield reward tokens in ERC20 such as WETHRebasing or USDB then these rewards will also be lost.

Proof of Concept

During deployment of the staking contract on Blast, the staking contract's gas yield mode should be configured to be able to claim accrued gas fees spent on the contract. We know that the bootstrap() function spawns the LockingMultiRewards staking contract below.

function bootstrap(uint256 minAmountOut) external onlyOwner onlyState(State.Closed) returns (address, address, uint256) {
        ...
 @>     staking = new LockingMultiRewards(pool, 30_000, 7 days, 13 weeks, address(this)); // spawns the staking contract
        ...
    }

However, in the LockingMultiRewards contract, the BlastYields.configureDefaultClaimables function does not exist in the LockingMultiRewards constructor to configure the staking contract's gas yield mode. Therefore, gas yield cannot be claimed LockingMultiRewards.

constructor(
        address _stakingToken,
        uint256 _lockingBoostMultiplerInBips,
        uint256 _rewardsDuration,
        uint256 _lockDuration,
        address _owner
    ) OperatableV2(_owner) {
        if (_lockingBoostMultiplerInBips <= BIPS) {
            revert ErrInvalidBoostMultiplier();
        }

        if (_lockDuration < MIN_LOCK_DURATION) {
            revert ErrInvalidLockDuration();
        }

        if (_rewardsDuration < MIN_REWARDS_DURATION) {
            revert ErrInvalidRewardDuration();
        }

        if (_lockDuration % _rewardsDuration != 0) {
            revert ErrInvalidDurationRatio();
        }

        stakingToken = _stakingToken;
        lockingBoostMultiplerInBips = _lockingBoostMultiplerInBips;
        rewardsDuration = _rewardsDuration;
        lockDuration = _lockDuration;

        // kocks are combined into the same `rewardsDuration` epoch. So, if
        // a user stake with locking every `rewardsDuration` this should reach the
        // maximum number of possible simultaneous because the first lock gets expired,
        // freeing up a slot.
        maxLocks = _lockDuration / _rewardsDuration;
    }

We believe this to be a mistake by the Abracadabra team as all other contracts that use substantial gas except for this contract had their gas yield mode configured/ The gas spent for this contract will be very substantial considering that users will spend substantial amounts of gas claiming from the staking contract. Therefore the Abracadabra is missing out the gas fees in this contract by not configuring the gas yield mode and possibly native yield in such as WETHRebasing or USDB if such tokens are used

Tools Used

Manual review + foundry

Configure the Blast gas yield mode for the LockingMultiRewards staking contract in the constructor and expose a function for admin to collect the yields. Additionally, also consider whether Blast points need to be configured here.

Assessed type

Other

#0 - c4-pre-sort

2024-03-15T12:27:01Z

141345 marked the issue as duplicate of #229

#1 - c4-judge

2024-03-29T03:29:36Z

thereksfour marked the issue as unsatisfactory: Invalid

#2 - c4-judge

2024-03-29T04:51:18Z

thereksfour marked the issue as not a duplicate

#3 - c4-judge

2024-03-29T04:51:41Z

thereksfour marked the issue as satisfactory

#4 - c4-judge

2024-03-29T05:12:52Z

thereksfour marked the issue as primary issue

#5 - c4-judge

2024-03-31T07:09:22Z

thereksfour marked the issue as selected for report

#6 - trust1995

2024-04-02T17:04:04Z

The Abracadabra team is aware that not all contracts are wrapped with YIELD wrappers, from the README: Some contracts were wrapped so it's usable with Blast L2 yield claiming It is up to the team to decide which ones they are interested in claiming yield from. The suggestion made in the submission is very reasonable, but it belongs to Analysis / QA as a potential improvement, rather than a material handling of funds issue.

#7 - rexjoseph

2024-04-03T08:59:37Z

Hi again @thereksfour!

Thank you for seeing the issue here! Just to reiterate, I'll touch on points already present in our submission.

The Abracadabra team configures gas yield for all contracts in the Blast L2 except for the LockingMultiRewards contract.

The team has implemented a fix for this issue now and as we can see in FACTS as opposed to Trust's assessment, they do really care about the yield and native claiming system on Blast. The fix can be found here: https://github.com/Abracadabra-money/abracadabra-money-contracts/pull/134/commits/6cb4bf1cd60dd08b08b8ee488252ce2c0500775d

Therefore, gas yields accrued on the LockingMultiRewards staking contract since deployment will be lost as it was never configured using BlastYields.configureDefaultClaimables

To hit on this point further, it is important to understand that the LockingMultiRewards contract will not ONLY be deployed on Arbitrum but also on Blast L2. Hence, yields on that contract will be a plenty ton as it will be one of the most used contracts within the Abracadabra protocol. You can see they now spawn up the LockingMultiRewards correctly just as we suggested in their fix:

staking = new BlastLockingMultiRewards(registry, feeTo, pool, 30_000, 7 days, 13 weeks, address(this));

In any case, the provided link contains all of the fixes as highlighted by this report and as always, we thank you for the fast judgement! @thereksfour

#8 - trust1995

2024-04-03T11:14:46Z

The team has implemented a fix for this issue now and as we can see in FACTS as opposed to Trust's assessment, they do really care about the yield and native claiming system on Blast.

Objection, I never said they don't care about the yield. I stated it is a recommendation-level "issue" to point which additional contracts they should wrap.

To hit on this point further, it is important to understand that the LockingMultiRewards contract will not ONLY be deployed on Arbitrum but also on Blast L2.

This is a useless mention because if it wasn't the case, the entire premise would be invalid. Instead it is a valid suggestion.

The existence of a fix and view of the sponsor absolutely does not affect the severity level of the issue and is a cheap fallacy to abuse, quite frankly.

#9 - Haxatron

2024-04-03T13:22:34Z

Our rationale on this being a Medium is that the protocol has intended to made all the contracts which will generate significant gas yield (and token yield) claimable, and we believe this can be reasonably inferred from the repository. Therefore, if the protocol has missed out on making the gas yield and token yield from the LockingMultiRewards claimable, we view that as a "leak of value" and would consider that as a Medium severity bug.

However, we also respect your viewpoint @Trust and we ask that the judge consider each viewpoint carefully before coming to a final decision. We shall not comment on this any further.

#10 - thereksfour

2024-04-04T15:20:02Z

I'll consider this an M about value leak.

Findings Information

🌟 Selected for report: DarkTower

Also found by: ether_sky

Labels

bug
2 (Med Risk)
disagree with severity
downgraded by judge
primary issue
satisfactory
selected for report
sponsor confirmed
sufficient quality report
M-16

Awards

930.9947 USDC - $930.99

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/blast/BlastOnboarding.sol#L101-L121

Vulnerability details

Impact

The bootstrap process relies on users locking up all their tokens as it will only use tokens that are marked locked in totals[MIM].locked and totals[USDB].locked.

BlastOnboardingBoot.sol#L96-L127

    function bootstrap(uint256 minAmountOut) external onlyOwner onlyState(State.Closed) returns (address, address, uint256) {
        if (pool != address(0)) {
            revert ErrAlreadyBootstrapped();
        }

=>      uint256 baseAmount = totals[MIM].locked;
=>      uint256 quoteAmount = totals[USDB].locked;
        MIM.safeApprove(address(router), type(uint256).max);
        USDB.safeApprove(address(router), type(uint256).max);

        (pool, totalPoolShares) = router.createPool(MIM, USDB, FEE_RATE, I, K, address(this), baseAmount, quoteAmount);

        if (totalPoolShares < minAmountOut) {
            revert ErrInsufficientAmountOut();
        }

        // Create staking contract
        // 3x boosting for locker, 7 days reward duration, 13 weeks lp locking
        // make this contract temporary the owner the set it as an operator
        // for permissionned `stakeFor` during the claiming process and then
        // transfer the ownership to the onboarding owner.
        staking = new LockingMultiRewards(pool, 30_000, 7 days, 13 weeks, address(this));
        staking.setOperator(address(this), true);
        staking.transferOwnership(owner);

        // Approve staking contract
        pool.safeApprove(address(staking), totalPoolShares);

        emit LogLiquidityBootstrapped(pool, address(staking), totalPoolShares);

        return (pool, address(staking), totalPoolShares);
    }

However, it is possible that these values can be zero because there is a cap on the amount of tokens that can be stored in the contract.

BlastOnboarding.sol#L101-L121

    function deposit(address token, uint256 amount, bool lock_) external whenNotPaused onlyState(State.Opened) onlySupportedTokens(token) {
        token.safeTransferFrom(msg.sender, address(this), amount);

        if (lock_) {
            totals[token].locked += amount;
            balances[msg.sender][token].locked += amount;
        } else {
            totals[token].unlocked += amount;
            balances[msg.sender][token].unlocked += amount;
        }

        totals[token].total += amount;

        if (caps[token] > 0 && totals[token].total > caps[token]) {
            revert ErrCapReached();
        }

        balances[msg.sender][token].total += amount;

        emit LogDeposit(msg.sender, token, amount, lock_);
    }

In the code above, notice that there is a cap on the amount of tokens that can be sent to the contract caps[token]. The problem here is that it is checked against the total token amount totals[token].total rather than the locked amount totals[token].locked.

Therefore a griefer can deposit tokens up to the caps[token] with lock_ = false. The result, is that no one else can deposit tokens into the contract.

During bootstrap, since these tokens are still considered unlocked, then totals[MIM].locked = 0 and totals[USDB].locked = 0, therefore there won't be any locked tokens available for the bootstrapping process.

Tools Used

Manual Review

Instead of checking caps[token] against totals[token].total, it should be checked against totals[token].locked.

Assessed type

Other

#0 - c4-pre-sort

2024-03-15T14:31:14Z

141345 marked the issue as sufficient quality report

#1 - 141345

2024-03-15T14:31:31Z

send cap amt

but the sender will lose a lot, and create another pool can solve it

#2 - c4-sponsor

2024-03-15T16:03:55Z

0xCalibur marked the issue as disagree with severity

#4 - c4-sponsor

2024-03-15T16:46:10Z

0xCalibur (sponsor) acknowledged

#5 - c4-sponsor

2024-03-15T16:46:13Z

0xCalibur (sponsor) confirmed

#6 - thereksfour

2024-03-29T08:26:28Z

Incorrect cap check, consider M

#7 - c4-judge

2024-03-29T08:26:35Z

thereksfour changed the severity to 2 (Med Risk)

#8 - c4-judge

2024-03-29T16:10:08Z

thereksfour marked the issue as satisfactory

#9 - c4-judge

2024-03-31T07:10:53Z

thereksfour marked the issue as selected for report

#10 - trust1995

2024-04-02T17:29:45Z

I believe the finding is actually invalid:

  • The cap is checking for total tokens because at any point, a token that is unlocked can become locked through the lock() function. Therefore, we treat all tokens deposits as "potentially locked" tokens, and prepare a perimeter around that.
  • Griefer can deposit their tokens in the contract for zero-yield, that's a tradeoff the protocol seems to be fine to live with.

In fact, the fix the team has applied here can now be abused by the first point to break the capacity invariant, by depositing with lock_=false, then calling lock(). @0xCalibur

This demonstrates the original code did not have a M-level issue, but is a design choice around how to properly limit the capacity.

#11 - rexjoseph

2024-04-03T08:40:11Z

Hi @thereksfour first off, thanks for the swift judgment ~ 24 hours is a great time to wrap this all up.

Now, I disagree with Trust's assessment of the issue here. As we already know, locked & unlocked tokens are different within the Abracadabra protocol. The locked tokens will facilitate the creation of MagicLP. The unlocked tokens on the other end will not.

The issue here is that the capacity for the locked tokens can and will be abused by users because anyone can deposit just as much as the amount of unlocked to grief the protocol of deposits and one way to fix this is for the protocol to bump up the cap. This is not an ideal fix in any way as the code would be unchanged but the issue persists. Hence, the recommendation to properly check caps against locked. Also, for the fact this state can be easily reached with one deposit from a malicious user, the MagicLP pool creation will not be possible. Downplaying this issue because you do not like the fix doesn't make the issue invalid in any way.

On the other hand, DoS/grief of a protocol function is considered top-level MEDIUM and in some cases HIGH in c4. And following that, In essence, this submission also satisfies all of that severity categorization.

Thank you for taking the time to go through this and seeing the issue here as the sponsor has already confirmed.

#12 - trust1995

2024-04-03T11:08:53Z

My argument is that there is a trade-off at play here and both options have their weaknesses but that does not make it an M level issue, but rather a design opinion.

The issue here is that the capacity for the locked tokens can and will be abused by users False assumption that users will "abuse" the protocol by depositing tokens for zero yield.

Capacity can always be lifted beyond such "grief" amounts being sent.

I respect your view, please respect mine and let the judge make the correct call.

#13 - Haxatron

2024-04-03T13:24:59Z

Our final thoughts on this is that we believe that this is not an intended design choice and not a tradeoff the protocol accepts because the sponsor has marked this as confirmed and fixed. Edit: And to add on we do not believe adjusting the cap is a mitigation to this because the protocol will miss out on tokens that users intended to be locked while capacity is reached.

We ask the judge to review each of our viewpoints here carefully before coming to a conclusion. We shall not comment on this any further.

#14 - 0xCalibur

2024-04-04T13:28:29Z

I believe the finding is actually invalid:

  • The cap is checking for total tokens because at any point, a token that is unlocked can become locked through the lock() function. Therefore, we treat all tokens deposits as "potentially locked" tokens, and prepare a perimeter around that.
  • Griefer can deposit their tokens in the contract for zero-yield, that's a tradeoff the protocol seems to be fine to live with.

In fact, the fix the team has applied here can now be abused by the first point to break the capacity invariant, by depositing with lock_=false, then calling lock(). @0xCalibur

This demonstrates the original code did not have a M-level issue, but is a design choice around how to properly limit the capacity.

True, the current fix doesn't resolve the issue. Here is the new mitigation: https://github.com/Abracadabra-money/abracadabra-money-contracts/pull/161

#15 - thereksfour

2024-04-04T15:06:33Z

I would still think it's a valid M. The purpose of the protocol is to have more locked tokens, and obviously in this issue wavering users and griefers may block users who want to lock.

#16 - c4-judge

2024-04-04T15:06:39Z

thereksfour marked the issue as primary issue

Awards

189.8 USDC - $189.80

Labels

bug
grade-a
QA (Quality Assurance)
sufficient quality report
edited-by-warden
Q-23

External Links

[L-01]: Removing supported tokens prevents users from withdrawing tokens from BlastOnboarding.

Due to the onlySupportedTokens(token) modifier, if the owner removes support for tokens, users can no longer withdraw their tokens and their funds will get stuck in the BlastOnboarding.

BlastOnboarding.sol#L132-L141

    function withdraw(address token, uint256 amount) external whenNotPaused onlySupportedTokens(token) {
        balances[msg.sender][token].unlocked -= amount;
        balances[msg.sender][token].total -= amount;
        totals[token].unlocked -= amount;
        totals[token].total -= amount;

        token.safeTransfer(msg.sender, amount);

        emit LogWithdraw(msg.sender, token, amount);
    }

Rescues will also not be able function for unsupported tokens.

BlastOnboarding.sol#L205-L212

    function rescue(address token, address to, uint256 amount) external onlyOwner {
        if (supportedTokens[token]) {
            revert ErrNotAllowed();
        }

        token.safeTransfer(to, amount);
        emit LogTokenRescue(token, to, amount);
    }

Therefore, these tokens will get stuck in the contract until it supports these tokens again which may not be ideal.

Consider removing onlySupportedTokens(token) modifier for withdrawals and rescues

[L-02]: FeeRateModel.sol, FeeRateModelImpl.sol, BlastTokenRegistry.sol do not configure gas yield.

The constructors for the following contracts do not configure gas yield on Blast:

  1. https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/blast/BlastTokenRegistry.sol

  2. https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/mimswap/auxiliary/FeeRateModel.sol

  3. https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/mimswap/auxiliary/FeeRateModelImpl.sol

Gas yield for these files won't be substantial so this is marked as low.

[L-03]: BlastMagicLP implementation do not configure gas yield.

The implementation BlastMagicLP does not configure the gas yield mode in the constructor.

BlastMagicLP.sol#L23-L33

    constructor(BlastTokenRegistry registry_, address feeTo_, address owner_) MagicLP(owner_) {
        if (feeTo_ == address(0)) {
            revert ErrZeroAddress();
        }
        if (address(registry_) == address(0)) {
            revert ErrZeroAddress();
        }

        registry = registry_;
        feeTo = feeTo_;
    }

Since there will still be operations carried out on the implementation contract for admin configuration purposes, it might be best to configure the yield mode by calling BlastYields.configureDefaultClaimables(address(this)) in the constructor so that any gas spent on implementation contract can be reclaimed.

[L-04] The maintainer fee model can be changed in the Factory

The maintainer fee model can be changed in the Factory for new MagicLPs deployed.

Factory.sol#L105-L112

    function setMaintainerFeeRateModel(IFeeRateModel maintainerFeeRateModel_) external onlyOwner {
        if (address(maintainerFeeRateModel_) == address(0)) {
            revert ErrZeroAddress();
        }

        maintainerFeeRateModel = maintainerFeeRateModel_;
        emit LogSetMaintainerFeeRateModel(maintainerFeeRateModel_);
    }

However, this affects new pools deployed and old pools will still use the old maintainer fee model leading to different pools potentially having different fee models which breaks consistency. The maintainer fee rate model contract already has a function to change the rate model implementation and therefore such a function in the factory isn't required.

FeeRateModel.sol#L57-L60

    /// @notice Set the fee rate implementation and default fee rate
    /// @param implementation_ The address of the fee rate implementation, use address(0) to disable
    function setImplementation(address implementation_) public onlyOwner {
        implementation = implementation_;
        emit LogImplementationChanged(implementation_);
    }

[L-05] MagicLP is neither pausable not upgradeable.

MagicLP is neither pausable nor upgradeable (it uses non-upgradeable clones). We don't believe this to be a good idea because if a pool-draining exploit was found it would be nearly impossible to protect all the funds still present in the MagicLP.

[L-06] Router does not verify whether LP provided is a MagicLP

For all swap functions in the Router, it doesn't validate that the lp address provided is an actual MagicLP. While we couldn't find a way to exploit this. Historically, this has been the cause of famous router hacks such as the famous Sushiswap hack - https://maxwelldulin.com/BlogPost/sushiswap-exploit-explained-2023

[L-07] MagicLpAggregator can't work with tokens with more than 18 decimals or if any token oracle returns more than 18 decimals

It will revert if any token oracle returns more than 18 decimals due to underflow in WAD - baseOracle.decimals() or WAD - quoteOracle.decimals().

MagicLpAggregator.sol#L38-L39

        uint256 baseAnswerNomalized = uint256(baseOracle.latestAnswer()) * (10 ** (WAD - baseOracle.decimals()));
        uint256 quoteAnswerNormalized = uint256(quoteOracle.latestAnswer()) * (10 ** (WAD - quoteOracle.decimals()));

It can also revert for tokens with more than 18 decimals due to underflow in (WAD - baseDecimals) or (WAD - quoteDecimals).

MagicLpAggregator.sol#L43-L44

        baseReserve = baseReserve * (10 ** (WAD - baseDecimals));
        quoteReserve = quoteReserve * (10 ** (WAD - quoteDecimals));

[R-01] Consider a clearer naming for the maximum boost multiplier that can be set in basis points

The BIPS variable caps the maximum boost multiplier at 100%. It could do well to be renamed to MAX_BIPS for better code readability.

- uint256 private constant BIPS = 10_000;
+ uint256 private constant MAX_BIPS = 10_000;

[N-01] Typo in the _udpateUserRewards function naming

As can be seen from the title of this log and in the codebase, the function to update user rewards has a typo and should be corrected.

function _udpateUserRewards(address user_, uint256 balance_, address token_, uint256 rewardPerToken_) internal {
    rewards[user_][token_] = _earned(user_, balance_, token_, rewardPerToken_);
    userRewardPerTokenPaid[user_][token_] = rewardPerToken_;
}
- function _udpateUserRewards(address user_, uint256 balance_, address token_, uint256 rewardPerToken_) internal {
+ function _updateUserRewards(address user_, uint256 balance_, address token_, uint256 rewardPerToken_) internal {
    rewards[user_][token_] = _earned(user_, balance_, token_, rewardPerToken_);
    userRewardPerTokenPaid[user_][token_] = rewardPerToken_;
}

#0 - c4-pre-sort

2024-03-15T15:00:47Z

141345 marked the issue as sufficient quality report

#2 - c4-judge

2024-03-29T16:32:54Z

thereksfour marked the issue as grade-b

#3 - rexjoseph

2024-04-03T10:01:30Z

Hi @thereksfour, I believe [L-06] is a duplicate of #86.

Also, looking at this report's value and comparing it to other LLM reports, for example, #110, #112, & #54, I believe this report provides more value than the above-mentioned QA reports and should be up there as grade-a. Nothing against the other reports frankly, and this is up to you as I believe this report should be re-evaluated.

Thanks!

#4 - c4-judge

2024-04-06T06:49:04Z

thereksfour marked the issue as grade-a

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