Coder Social home page Coder Social logo

2023-10-looksrare-judging's Introduction

Issue H-1: _killWoundedAgents

Source: #21

Found by

ast3ros, cergyk, klaus, mstpr-brainbot

If an agent healed is wounded again, the agent will die from the previous wound that was healed. The user spends LOOKS tokens to heal and success to heal, but as the result, the agent will die.

Vulnerability Detail

The _killWoundedAgents function only checks the status of the agent, not when it was wounded.

    function _killWoundedAgents(
        uint256 roundId,
        uint256 currentRoundAgentsAlive
    ) private returns (uint256 deadAgentsCount) {
        ...
        for (uint256 i; i < woundedAgentIdsCount; ) {
            uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];

            uint256 index = agentIndex(woundedAgentId);
@>          if (agents[index].status == AgentStatus.Wounded) {
                ...
            }

            ...
        }

        emit Killed(roundId, woundedAgentIds);
    }

So when fulfillRandomWords kills agents that were wounded and unhealed at round currentRoundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD, it will also kill the agent who was healed and wounded again after that round.

Also, since fulfillRandomWords first draws the new wounded agents before kills agents, in the worst case scenario, agent could die immediately after being wounded in this round.

if (activeAgents > NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {
@>  uint256 woundedAgents = _woundRequestFulfilled(
        currentRoundId,
        currentRoundAgentsAlive,
        activeAgents,
        currentRandomWord
    );

    uint256 deadAgentsFromKilling;
    if (currentRoundId > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD) {
@>      deadAgentsFromKilling = _killWoundedAgents({
            roundId: currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD),
            currentRoundAgentsAlive: currentRoundAgentsAlive
        });
    }

This is the PoC test code. You can add it to the Infiltration.fulfillRandomWords.t.sol file and run it.

function test_poc() public {

    _startGameAndDrawOneRound();

    uint256[] memory randomWords = _randomWords();
    uint256[] memory woundedAgentIds;

    for (uint256 roundId = 2; roundId <= ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD + 1; roundId++) {

        if(roundId == 2) { // heal agent. only woundedAgentIds[0] dead.
            (woundedAgentIds, ) = infiltration.getRoundInfo({roundId: 1});
            assertEq(woundedAgentIds.length, 20);

            _drawXRounds(1);

            _heal({roundId: 3, woundedAgentIds: woundedAgentIds});

            _startNewRound();

            // everyone except woundedAgentIds[0] is healed
            uint256 agentIdThatWasKilled = woundedAgentIds[0];

            IInfiltration.HealResult[] memory healResults = new IInfiltration.HealResult[](20);
            for (uint256 i; i < 20; i++) {
                healResults[i].agentId = woundedAgentIds[i];

                if (woundedAgentIds[i] == agentIdThatWasKilled) {
                    healResults[i].outcome = IInfiltration.HealOutcome.Killed;
                } else {
                    healResults[i].outcome = IInfiltration.HealOutcome.Healed;
                }
            }

            expectEmitCheckAll();
            emit HealRequestFulfilled(3, healResults);

            expectEmitCheckAll();
            emit RoundStarted(4);

            randomWords[0] = (69 * 10_000_000_000) + 9_900_000_000; // survival rate 99%, first one gets killed

            vm.prank(VRF_COORDINATOR);
            VRFConsumerBaseV2(address(infiltration)).rawFulfillRandomWords(_computeVrfRequestId(3), randomWords);

            for (uint256 i; i < woundedAgentIds.length; i++) {
                if (woundedAgentIds[i] != agentIdThatWasKilled) {
                    _assertHealedAgent(woundedAgentIds[i]);
                }
            }

            roundId += 2; // round 2, 3 used for healing
        }

        _startNewRound();

        // Just so that each round has different random words
        randomWords[0] += roundId;

        if (roundId == ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD + 1) { // wounded agents at round 1 are healed, only woundedAgentIds[0] was dead.
            (uint256[] memory woundedAgentIdsFromRound, ) = infiltration.getRoundInfo({
                roundId: uint40(roundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD)
            });

            // find re-wounded agent after healed
            uint256[] memory woundedAfterHeal = new uint256[](woundedAgentIds.length);
            uint256 totalWoundedAfterHeal;
            for (uint256 i; i < woundedAgentIds.length; i ++){
                uint256 index = infiltration.agentIndex(woundedAgentIds[i]);
                IInfiltration.Agent memory agent = infiltration.getAgent(index);
                if (agent.status == IInfiltration.AgentStatus.Wounded) {
                    woundedAfterHeal[i] = woundedAgentIds[i]; // re-wounded agent will be killed
                    totalWoundedAfterHeal++;
                }
                else{
                    woundedAfterHeal[i] = 0; // set not wounded again 0
                }

            }
            expectEmitCheckAll();
            emit Killed(roundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD, woundedAfterHeal);
        }

        expectEmitCheckAll();
        emit RoundStarted(roundId + 1);

        uint256 requestId = _computeVrfRequestId(uint64(roundId));
        vm.prank(VRF_COORDINATOR);
        VRFConsumerBaseV2(address(infiltration)).rawFulfillRandomWords(requestId, randomWords);
    }
}

Impact

The user pays tokens to keep the agent alive, but agent will die even if agent success to healed. The user has lost tokens and is forced out of the game.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1489

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1130-L1143

Tool used

Manual Review

Recommendation

Check woundedAt at _killWoundedAgents

    function _killWoundedAgents(
        uint256 roundId,
        uint256 currentRoundAgentsAlive
    ) private returns (uint256 deadAgentsCount) {
        ...
        for (uint256 i; i < woundedAgentIdsCount; ) {
            uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];

            uint256 index = agentIndex(woundedAgentId);
-           if (agents[index].status == AgentStatus.Wounded) {
+           if (agents[index].status == AgentStatus.Wounded && agents[index].woundedAt == roundId) {
                ...
            }

            ...
        }

        emit Killed(roundId, woundedAgentIds);
    }

Discussion

0xhiroshi

https://github.com/LooksRare/contracts-infiltration/pull/148

0xhiroshi

https://github.com/LooksRare/contracts-infiltration/pull/164

SergeKireev

Fix LGTM

Issue H-2: Winning agent id may be uninitialized when game is over, locking grand prize

Source: #31

Found by

cergyk, detectiveking In the Infiltration contract, the agents mapping holds all of the agents structs, and encodes the ranking of the agents (used to determine prizes at the end of the game).

This mapping records are lazily initialized when two agents are swapped (an agent is either killed or escapes):

  • The removed agent goes to the end of currently alive agents array with the status Killed/Escaped and its agentId is set
  • The last agent of the currently alive agents array is put in place of the previously removed agent and its agentId is set

This is the only moment when the agentId of an agent record is set.

This means that if the first agent in the array ends up never being swapped, it keeps its agentId as zero, and the grand prize is unclaimable.

Vulnerability Detail

We can see in the implementation of claimGrandPrize that: https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L658

The field Agent.agentId of the struct is used to determine if the caller can claim. Since the id is zero, and it is and invalid id for an agent, there is no owner for it and the condition:

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1666-L1670

Always reverts.

Impact

The grand prize ends up locked/unclaimable by the winner

Code Snippet

Tool used

Manual Review

Recommendation

In claimGrandPrize use 1 as the default if agents[1].agentId == 0:

function claimGrandPrize() external nonReentrant {
    _assertGameOver();
    uint256 agentId = agents[1].agentId;
+   if (agentId == 0)
+       agentId = 1;
    _assertAgentOwnership(agentId);

    uint256 prizePool = gameInfo.prizePool;

    if (prizePool == 0) {
        revert NothingToClaim();
    }

    gameInfo.prizePool = 0;

    _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, prizePool, gasleft());

    emit PrizeClaimed(agentId, address(0), prizePool);
}

Discussion

0xhiroshi

https://github.com/LooksRare/contracts-infiltration/pull/153

0xhiroshi

https://github.com/LooksRare/contracts-infiltration/pull/178

@nevillehuang

SergeKireev

Fix LGTM

Issue H-3: Attacker can steal reward of actual winner by force ending the game

Source: #98

Found by

0xReiAyanami, SilentDefendersOfDeFi, cergyk, mstpr-brainbot

A malicious user can force win the game by escaping all but one wounded agent, and steal the grand price.

Vulnerability Detail

Currently following scenario is possible: There is an attacker owning some lower index agents and some higher index agents. There is a normal user owing one agent with an index between the attackers agents. If one of the attackers agents with an lower index gets wounded, he can escape all other agents and will instantly win the game, even if the other User has still one active agent.

This is possible because because the winner is determined by the agent index, and escaping all agents at once wont kill the wounded agent because the game instantly ends.

Following check inside startNewRound prevents killing of wounded agents by starting a new round:

uint256 activeAgents = gameInfo.activeAgents;
        if (activeAgents == 1) {
            revert GameOver();
        }

Following check inside of claimPrize pays price to first ID agent:

uint256 agentId = agents[1].agentId;
_assertAgentOwnership(agentId);

See following POC:

POC

Put this into Infiltration.mint.t.sol and run forge test --match-test forceWin -vvv

function test_forceWin() public {
        address attacker = address(1337);

        //prefund attacker and user1
        vm.deal(user1, PRICE * MAX_MINT_PER_ADDRESS);
        vm.deal(attacker, PRICE * MAX_MINT_PER_ADDRESS);

        // MINT some agents
        vm.warp(_mintStart());
        // attacker wants to make sure he owns a bunch of agents with low IDs!!
        vm.prank(attacker);
        infiltration.mint{value: PRICE * 30}({quantity: 30});
        // For simplicity we mint only 1 agent to user 1 here, but it could be more, they could get wounded, etc.
        vm.prank(user1);
        infiltration.mint{value: PRICE *1}({quantity: 1});
        //Attacker also wants a bunch of agents with the highest IDs, as they are getting swapped with the killed agents (move forward)
        vm.prank(attacker);
        infiltration.mint{value: PRICE * 30}({quantity: 30});
    
        vm.warp(_mintEnd());

        //start the game
        vm.prank(owner);
        infiltration.startGame();

        vm.prank(VRF_COORDINATOR);
        uint256[] memory randomWords = new uint256[](1);
        randomWords[0] = 69_420;
        VRFConsumerBaseV2(address(infiltration)).rawFulfillRandomWords(_computeVrfRequestId(1), randomWords);
        // Now we are in round 2 we do have 1 wounded agent (but we can imagine any of our agent got wounded, doesn´t really matter)
        
        // we know with our HARDCODED RANDOMNESS THAT AGENT 3 gets wounded!!

        // Whenever we get in a situation, that we own all active agents, but 1 and our agent has a lower index we can instant win the game!!
        // This is done by escaping all agents, at once, except the lowest index
        uint256[] memory escapeIds = new uint256[](59);
        escapeIds[0] = 1;
        escapeIds[1] = 2;
        uint256 i = 4; //Scipping our wounded AGENT 3
        for(; i < 31;) {
            escapeIds[i-2] = i;
            unchecked {++i;}
        }
        //skipping 31 as this owned by user1
        unchecked {++i;}
        for(; i < 62;) {
            escapeIds[i-3] = i;
            unchecked {++i;}
        }
        vm.prank(attacker);
        infiltration.escape(escapeIds);

        (uint16 activeAgents, uint16 woundedAgents, , uint16 deadAgents, , , , , , , ) = infiltration.gameInfo();
        console.log("Active", activeAgents);
        assertEq(activeAgents, 1);
        // This will end the game instantly.
        //owner should not be able to start new round
        vm.roll(block.number + BLOCKS_PER_ROUND);
        vm.prank(owner);
        vm.expectRevert();
        infiltration.startNewRound();

        //Okay so the game is over, makes sense!
        // Now user1 has the only active AGENT, so he should claim the grand prize!
        // BUT user1 cannot
        vm.expectRevert(IInfiltration.NotAgentOwner.selector);
        vm.prank(user1);
        infiltration.claimGrandPrize();

        //instead the attacker can:
        vm.prank(attacker);
        infiltration.claimGrandPrize();
        

Impact

Attacker can steal the grand price of the actual winner by force ending the game trough escapes.

This also introduces problems if there are other players with wounded agents but lower < 50 TokenID, they can claim prices for wounded agents, which will break parts of the game logic.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L589-L592

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L656-L672

Tool used

Manual Review

Recommendation

Start a new Round before the real end of game to clear all wounded agents and reorder IDs.

Discussion

0xhiroshi

https://github.com/LooksRare/contracts-infiltration/pull/154

SergeKireev

Fix LGTM

Issue M-1: Agents with Healing Opportunity Will Be Terminated Directly if The escape Reduces activeAgents to the Number of NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS or Fewer

Source: #43

Found by

Krace, SilentDefendersOfDeFi, detectiveking, mstpr-brainbot

Wounded Agents face the risk of losing their last opportunity to heal and are immediately terminated if certain Active Agents decide to escape.

Vulnerability Detail

In each round, agents have the opportunity to either escape or heal before the _requestForRandomness function is called. However, the order of execution between these two functions is not specified, and anyone can be executed at any time just before startNewRound. Typically, this isn't an issue. However, the problem arises when there are only a few Active Agents left in the game.

On one hand, the heal function requires that the number of gameInfo.activeAgents is greater than NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS.

    function heal(uint256[] calldata agentIds) external nonReentrant {
        _assertFrontrunLockIsOff();
//@audit If there are not enough activeAgents, heal is disabled
        if (gameInfo.activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {
            revert HealingDisabled();
        }

On the other hand, the escape function will directly set the status of agents to "ESCAPE" and reduce the count of gameInfo.activeAgents.

   function escape(uint256[] calldata agentIds) external nonReentrant {
        _assertFrontrunLockIsOff();

        uint256 agentIdsCount = agentIds.length;
        _assertNotEmptyAgentIdsArrayProvided(agentIdsCount);

        uint256 activeAgents = gameInfo.activeAgents;
        uint256 activeAgentsAfterEscape = activeAgents - agentIdsCount;
        _assertGameIsNotOverAfterEscape(activeAgentsAfterEscape);

        uint256 currentRoundAgentsAlive = agentsAlive();

        uint256 prizePool = gameInfo.prizePool;
        uint256 secondaryPrizePool = gameInfo.secondaryPrizePool;
        uint256 reward;
        uint256[] memory rewards = new uint256[](agentIdsCount);

        for (uint256 i; i < agentIdsCount; ) {
            uint256 agentId = agentIds[i];
            _assertAgentOwnership(agentId);

            uint256 index = agentIndex(agentId);
            _assertAgentStatus(agents[index], agentId, AgentStatus.Active);

            uint256 totalEscapeValue = prizePool / currentRoundAgentsAlive;
            uint256 rewardForPlayer = (totalEscapeValue * _escapeMultiplier(currentRoundAgentsAlive)) /
                ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;
            rewards[i] = rewardForPlayer;
            reward += rewardForPlayer;

            uint256 rewardToSecondaryPrizePool = (totalEscapeValue.unsafeSubtract(rewardForPlayer) *
                _escapeRewardSplitForSecondaryPrizePool(currentRoundAgentsAlive)) / ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;

            unchecked {
                prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;
            }
            secondaryPrizePool += rewardToSecondaryPrizePool;

            _swap({
                currentAgentIndex: index,
                lastAgentIndex: currentRoundAgentsAlive,
                agentId: agentId,
                newStatus: AgentStatus.Escaped
            });

            unchecked {
                --currentRoundAgentsAlive;
                ++i;
            }
        }

        // This is equivalent to
        // unchecked {
        //     gameInfo.activeAgents = uint16(activeAgentsAfterEscape);
        //     gameInfo.escapedAgents += uint16(agentIdsCount);
        // }

Threrefore, if the heal function is invoked first then the corresponding Wounded Agents will be healed in function fulfillRandomWords. If the escape function is invoked first and the number of gameInfo.activeAgents becomes equal to or less than NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS, the heal function will be disable. This obviously violates the fairness of the game.

Example

Consider the following situation:

After Round N, there are 100 agents alive. And, 1 Active Agent wants to escape and 10 Wounded Agents want to heal.

  • Round N:
    • Active Agents: 51
    • Wounded Agents: 49
    • Healing Agents: 0

According to the order of execution, there are two situations. Please note that the result is calculated only after _healRequestFulfilled, so therer are no new wounded or dead agents

First, invoking escape before heal. heal is disable and all Wounded Agents are killed because there are not enough Active Agents.

  • Round N+1:
    • Active Agents: 50
    • Wounded Agents: 0
    • Healing Agents: 0

Second, invoking heal before escape. Suppose that heal saves 5 agents, and we got:

  • Round N+1:
    • Active Agents: 55
    • Wounded Agents: 39
    • Healing Agents: 0

Obviously, different execution orders lead to drastically different outcomes, which affects the fairness of the game.

Impact

If some Active Agents choose to escape, causing the count of activeAgents to become equal to or less than NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS, the Wounded Agents will lose their final chance to heal themselves.

This situation can significantly impact the game's fairness. The Wounded Agents would have otherwise had the opportunity to heal themselves and continue participating in the game. However, the escape of other agents leads to their immediate termination, depriving them of that chance.

Code Snippet

Heal will be disabled if there are not enout activeAgents. Infiltration.sol#L804

Escape will directly reduce the activeAgents. Infiltration.sol#L769

Tool used

Manual Review

Recommendation

It is advisable to ensure that the escape function is always called after the heal function in every round. This guarantees that every wounded agent has the opportunity to heal themselves when there are a sufficient number of activeAgents at the start of each round. This approach can enhance fairness and gameplay balance.

Discussion

0xhiroshi

This is a valid PvP game strategy.

nevillehuang

I can see sponsors view of disputing these issues given this protocol focuses on a PVP game. The difference between this two #43 and #57 and issue #98 is because #98 actually triggers an invalid game state and #84 truly skews the odds, but #43 and #57 don't fall into either categories.

However, due to a lack of concrete details on PVP strategies, I also see watsons point of view of how the use of escape() and heal() can cause unfair game mechanics. While I also understand the sponsor's view of difficulty in predicting PVP strategies, I think it could have been avoided by including this as potential risks in the accepted risks/known issues of sherlocks contest details (which is the source of truth for contest details).

As such, I am going to keep #43 and #57 as medium severity findings, given the attack path requires specific conditions that would otherwise have been valid PVP strategies.

0xhiroshi

We won't further dispute this issue and we won't fix it.

nevillehuang

After further considerations, going to mark this issue as invalid due to the analysis of the following test. It supports the sponsor claim that instant killing of wounded agents once active agents fall below NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS (50) is a valid PVP strategy.

  1. It first simulates active agents falling to 51
  2. Then it attempts to heal 3 wounded agents (ids: 8534, 3214 and 6189)
  3. It then escapes 2 agents to make active agents fall below 49
  4. A new round is started, instantly killing all wounded agents

Only #29 and #34 mentions the other root cause of instantly ending the game with escape() (which allows immediate claiming of grand prize) so this 2 issues will be dupped with #98.

Issue M-2: Wound agent can't invoke heal in the next round

Source: #44

Found by

coffiasd, lil.eth, mstpr-brainbot, pontifex According to the document:

if a user dies on round 12. The first round they can heal is round 13 However incorrect current round id check led to users being unable to invoke the heal function in the next round.

Vulnerability Detail

Assume players being marked as wounded in the round 12 , players cannot invoke heal in the next round 13

    function test_heal_in_next_round_v1() public {
        _startGameAndDrawOneRound();

        _drawXRounds(11);


        (uint256[] memory woundedAgentIds, ) = infiltration.getRoundInfo({roundId: 12});

        address agentOwner = _ownerOf(woundedAgentIds[0]);
        looks.mint(agentOwner, HEAL_BASE_COST);

        vm.startPrank(agentOwner);
        _grantLooksApprovals();
        looks.approve(TRANSFER_MANAGER, HEAL_BASE_COST);

        uint256[] memory agentIds = new uint256[](1);
        agentIds[0] = woundedAgentIds[0];

        uint256[] memory costs = new uint256[](1);
        costs[0] = HEAL_BASE_COST;

        //get gameInfo
        (,,,,,uint40 currentRoundId,,,,,) = infiltration.gameInfo();
        assert(currentRoundId == 13);

        //get agent Info
        IInfiltration.Agent memory agentInfo = infiltration.getAgent(woundedAgentIds[0]);
        assert(agentInfo.woundedAt == 12);

        //agent can't invoke heal in the next round.
        vm.expectRevert(IInfiltration.HealingMustWaitAtLeastOneRound.selector);
        infiltration.heal(agentIds);
    }

Impact

User have to wait for 1 more round which led to the odds for an Agent to heal successfully start at 99% at Round 1 reduce to 98% at Round 2.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L843#L847

    // No need to check if the heal deadline has passed as the agent would be killed
    unchecked {
        if (currentRoundId - woundedAt < 2) {
            revert HealingMustWaitAtLeastOneRound();
        }
    }

Tool used

Manual Review

Recommendation

    // No need to check if the heal deadline has passed as the agent would be killed
    unchecked {
-       if (currentRoundId - woundedAt < 2) {
-       if (currentRoundId - woundedAt < 1) {
            revert HealingMustWaitAtLeastOneRound();
        }
    }

Discussion

nevillehuang

@0xhiroshi Any reason why you disagree with the severity? It does mentioned in the docs that the first round user can heal is right after the round his agent is wounded. The above PoC also shows how a user cannot heal a wounded agent at round 13 when his agent was wounded at round 12.

0xhiroshi

https://github.com/LooksRare/contracts-infiltration/pull/151

SergeKireev

Fix LGTM

Issue M-3: Index values selected in _woundRequestFulfilled() are not uniformly distributed.

Source: #84

Found by

0xGoodess, Tricko, cergyk, detectiveking Index values selected in _woundRequestFulfilled() are not uniformly distributed. Indexes right next to wounded agents are more likely to be selected in the subsequent iterations, leading to bias in the distribution of wounded agents.

Vulnerability Detail

At the end of each round, the function _woundRequestFulfilled() is called, which uses uses the randomWord obtained from the VRF to select which agents should be marked as wounded. This selection process is carried out by performing a modulo operation on the randomWord with respect to the number of agents currently alive in the round, and then adding 1 to the result. The resulting value corresponds to the index of the agent to be designated as wounded, as illustrated in the code snippet section.

However, if the resulting index corresponds to an agent who is already wounded, the else branch is executed, where 1 is added to the randomWord for the next iteration of the loop. This is where the bias is introduced, because in the next iteration, the woundedAgentIndex will be the current woundedAgentIndex plus 1. As can be seen below:

$$ (A + 1) \bmod M $$

$$ ((A \bmod M) + (1 \bmod M)) \bmod M $$

As M > 1, we can simplify to

$$ ((A \bmod M) + 1) \bmod M $$

For $(A \bmod M) + 1$ less than $M$, we have

$$ (A + 1) \bmod M = (A \bmod M) + 1 $$

So with the exception of when randomWord overflows or (randomWord % currentRoundAgentsAlive + 1) >= currentRoundAgentsAlive, (randomWord + 1) % currentRoundAgentsAlive will be equal to (randomWord % currentRoundAgentsAlive) + 1.

Consequently, when the else branch is triggered, the next woundedAgentIndex will be ( previous woundedAgentIndex+ 1) from the last loop iteration (besides the two exceptions specified above). Therefore the agent at the next index will also be marked as wounded. As a result of this pattern, agents whose indexes are immediately next to an already wounded agent are more likely to be wounded than the remaining agents.

Consider the representative example below, albeit on a smaller scale (8 agents) to facilitate explanation. The initial state is represented in the table below:

Index 1 2 3 4 5 6 7 8
Agents Active Active Active Active Active Active Active Active
Probabilities 0.125 0.125 0.125 0.125 0.125 0.125 0.125 0.125

In the initial iteration of _woundRequestFulfilled(), assume that index 2 is selected. As expected from function logic, for the next iteration a new randomWord will be generated, resulting in a new index within the range of 1 to 8, all with equal probabilities. However now not all agents have an equal likelihood of being wounded. This disparity arises because both 3 and 2 (due to the else branch in the code above) will lead to the agent at index 3 being wounded.

Index 1 2 3 4 5 6 7 8
Agents Active Wounded Active Active Active Active Active Active
Probabilities 0.125 - 0.25 0.125 0.125 0.125 0.125 0.125

Now suppose that agents at index 2 and 3 are wounded. Following from the explanations above, index 4 has three times the chance of being wounded.

Index 1 2 3 4 5 6 7 8
Agents Active Wounded Wounded Active Active Active Active Active
Probabilities 0.125 - - 0.375 0.125 0.125 0.125 0.125

Impact

The distribution of wounded status among indexes is not uniform. Indexes subsequent to agents already wounded are more likely to be selected, introducing unfairness in the distribution of wounded agents.

This is particularly problematic because indexes that are close to each other are more likely to owned by the same address, due to the fact that when minting multiple agents, they are created in sequential indexes. Consequently, if an address has one of its agents marked as wounded, it becomes more probable that additional agents owned by the same address will also be marked as wounded. This creates an unfair situation to users.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1404-L1468

Tool used

Manual Review

Recommendation

Consider eliminating the else and calling _nextRandomWord(randomWord) at the end of the loop iteration, so a new randomWord is generated each time. As shown in the diff below:

diff --git a/Infiltration.sol b/Infiltration.mod.sol
index 31af961..1c43c31 100644
--- a/Infiltration.sol
+++ b/Infiltration.mod.sol
@@ -1451,15 +1451,9 @@ contract Infiltration is
                     ++i;
                     currentRoundWoundedAgentIds[i] = uint16(woundedAgentId);
                 }
-
-                randomWord = _nextRandomWord(randomWord);
-            } else {
-                // If no agent is wounded using the current random word, increment by 1 and retry.
-                // If overflow, it will wrap around to 0.
-                unchecked {
-                    ++randomWord;
-                }
             }
+
+            randomWord = _nextRandomWord(randomWord);
         }

         currentRoundWoundedAgentIds[0] = uint16(woundedAgentsCount);

Discussion

0xhiroshi

https://github.com/LooksRare/contracts-infiltration/pull/152

SergeKireev

Fix LGTM

Issue M-4: fulfillRandomWords() could revert under certain circumstances

Source: #136

Found by

ge6a, klaus, p-tsanev

According the documentation of Chainlink VRF the max gas limit for the VRF coordinator is 2 500 000. This means that the max gas that fulfillRandomWords() can use is 2 500 000 and if it is exceeded the function would revert. I have constructed a proof of concept that demonstrates it is possible to have a scenario in which fulfillRandomWords reverts and thereby disrupts the protocol's work.

Vulnerability Detail

Crucial part of my POC is the variable AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS. I communicated with the protocol's team that they plan to set it to 20 initially but it is possible to have a different value for it in future. For the POC i used 30.

function test_fulfillRandomWords_revert() public {
        _startGameAndDrawOneRound();

        _drawXRounds(48);
        
        uint256 counter = 0;
        uint256[] memory wa = new uint256[](30);
        uint256 totalCost = 0;

        for (uint256 j=2; j <= 6; j++) 
        {
            (uint256[] memory woundedAgentIds, ) = infiltration.getRoundInfo({roundId: j});

            uint256[] memory costs = new uint256[](woundedAgentIds.length);
            for (uint256 i; i < woundedAgentIds.length; i++) {
                costs[i] = HEAL_BASE_COST;
                wa[counter] = woundedAgentIds[i];
                counter++;
                if(counter > 29) break;
            }

            if(counter > 29) break;
        }
        
        
        totalCost = HEAL_BASE_COST * wa.length;
        looks.mint(user1, totalCost);

        vm.startPrank(user1);
        _grantLooksApprovals();
        looks.approve(TRANSFER_MANAGER, totalCost);


        infiltration.heal(wa);
        vm.stopPrank();

        _drawXRounds(1);
    }

I put this test into Infiltration.startNewRound.t.sol and used --gas-report to see that the gas used for fulfillRandomWords exceeds 2 500 000.

Impact

DOS of the protocol and inability to continue the game.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1096-L1249 https://docs.chain.link/vrf/v2/subscription/supported-networks/#ethereum-mainnet https://docs.chain.link/vrf/v2/security#fulfillrandomwords-must-not-revert

Tool used

Manual Review

Recommendation

A couple of ideas :

  1. You can limit the value of AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS to a small enough number so that it is 100% sure it will not reach the gas limit.

  2. Consider simply storing the randomness and taking more complex follow-on actions in separate contract calls as stated in the "Security Considerations" section of the VRF's docs.

Discussion

nevillehuang

Accepted risks, the KEYWORDS here are:

  • periods of network congestion --> this causes the hard code gas fallback to revert --> accepted risk
  • Any reason causing randomness request to not be fufilled --> If request for randomness is not fufilled due to ANY reason, even if highilighted in a submission, it is not a accepted finding since it is an accepted risk LooksRare are willing to take

In the case of extended periods of network congestion or any reason causing the randomness request not to be fulfilled, the contract owner is able to withdraw everything after approximately 36 hours.

gstoyanovbg

Escalate I disagree with the comment of @nevillehuang . Imagine a situation in which you start a game with 10,000 participants, go through many rounds, and at one point, the game stops and cannot continue. As far as I understand, the judge's claim is that the funds can be withdrawn after a certain time, which is true. However, will the gas fees be returned to all users who have healed agents or traded them on the open market in some way? And what about the time that the players have devoted to winning a game that suddenly stops due to a bug and cannot continue? Every player may have claims for significant missed benefits (and losses from gas fees). Now, imagine that this continues to happen again and again in some of the subsequent games. Will anyone even invest time and resources to play this game? My request to the judges is to review my report again because it has nothing to do with 'periods of network congestion' as stated and this issue is not listed under the "Accepted risks" section. Thanks. P.S During the contest i discussed this with the sponsor and confirmed that this is an issue (probably doesn't matter but wanted to mention it)

sherlock-admin2

Escalate I disagree with the comment of @nevillehuang . Imagine a situation in which you start a game with 10,000 participants, go through many rounds, and at one point, the game stops and cannot continue. As far as I understand, the judge's claim is that the funds can be withdrawn after a certain time, which is true. However, will the gas fees be returned to all users who have healed agents or traded them on the open market in some way? And what about the time that the players have devoted to winning a game that suddenly stops due to a bug and cannot continue? Every player may have claims for significant missed benefits (and losses from gas fees). Now, imagine that this continues to happen again and again in some of the subsequent games. Will anyone even invest time and resources to play this game? My request to the judges is to review my report again because it has nothing to do with 'periods of network congestion' as stated and this issue is not listed under the "Accepted risks" section. Thanks. P.S During the contest i discussed this with the sponsor and confirmed that this is an issue (probably doesn't matter but wanted to mention it)

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

PlamenTSV

Escalate My issue 107 is the same and I agree with the above statement, as this is not an occurance that can randomly happen once, depending on potential parameter changes it can become extremely likely to happen every time. If every game has a high chance to get permanently dosed, even if emergency withdrawal is available, the game itself becomes unplayable. The accepted risks cover network congestion and the 36 period, but that period does not solve the insolvency, it just refunds some funds, not even everything that costed players and congestion is not the cause, but the badly gas optimized function.

sherlock-admin2

Escalate My issue 107 is the same and I agree with the above statement, as this is not an occurance that can randomly happen once, depending on potential parameter changes it can become extremely likely to happen every time. If every game has a high chance to get permanently dosed, even if emergency withdrawal is available, the game itself becomes unplayable. The accepted risks cover network congestion and the 36 period, but that period does not solve the insolvency, it just refunds some funds, not even everything that costed players and congestion is not the cause, but the badly gas optimized function.

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

nevillehuang

This is the only comment I will make about this submission, and I will leave the rest up to @nasri136 @Evert0x .

I agree this is a valid issue, but the fact is in the accepted risk section it mentions that ANY and i repeat ANY issue not just network congestion causing randomness request to fail is an accepted risk. If the game is not completed, then admin can always call emergencyWithdraw(), so as long as this function is not DoSed (which is not possible unless game has ended), then I am simply following sherlocks rules:

Hierarchy of truth: Contest README > Sherlock rules for valid issues > Historical decisions. While considering the validity of an issue in case of any conflict the sources of truth are prioritized in the above order. For example: In case of conflict between Sherlock rules vs Sherlock's historical decision, Sherlock criteria for issues must be considered the source of truth. In case of conflict between information in the README vs Sherlock rules, the README overrides Sherlock rules.

And yes private/discord messages does not matter:

Discord messages or DM screenshots are not considered sources of truth while judging an issue/escalation especially if they are conflicting with the contest README.

gstoyanovbg

I understand your point. However, still believe that the 'Accepted Risks' section is not formulated well enough, and this case should definitely be excluded from its scope. I am aware of Sherlock's rules, but in my opinion, there are nuances even in them, and they cannot be 100% accurate for all situations. I see that report #107 by @PlamenTSV, which is same issue as this, has labels "Sponsor Confirmed" and "Will fix." Also, @0xhiroshi has made a pull request for a fix which makes me happy because at the end of the day our goal is to have a secure, bug free protocol. However, it sounds strange to claim that a vulnerability is an "accepted risk" but at the same time to fix it. Looking forward for the final decision of the judges. If you have any questions, I am ready to assist.

nevillehuang

@gstoyanovbg I totally get your point too and agree with you. If I didn’t have to follow sherlock rules, I would have side with the watsons and validated this finding. But the fact that in the accepted risks section they used the word ANY is too strong of a word for me to ignore.

PlamenTSV

Don't sherlock rules serve as a guideline? I hope most if not every watson that has read this issue agrees it is valid. The judges agree it is valid by severity and even the sponsors themselves want to fix it and deem it a valid finding. I think the reason the README is like this is because the protocol team did not expect that their protocol could have a DoS of this caliber - thus why they overlook the README and confirm the issue. It would be a shame to get robbed of a valid finding with no reward, worse ratio for the platform, but for the protocol to aknowledge and fix. There should be some kind of workaround for these kinds of scenarios. I believe that would be most fair and I hope you agree. If we could get a sherlock admin to give clarity to the situation it would be appreciated, thanks to everyone in the comments.

PlamenTSV

This is the only comment I will make about this submission, and I will leave the rest up to @nasri136 @Evert0x .

I agree this is a valid issue, but the fact is in the accepted risk section it mentions that ANY and i repeat ANY issue not just network congestion causing randomness request to fail is an accepted risk. If the game is not completed, then admin can always call emergencyWithdraw(), so as long as this function is not DoSed (which is not possible unless game has ended), then I am simply following sherlocks rules:

Hierarchy of truth: Contest README > Sherlock rules for valid issues > Historical decisions. While considering the validity of an issue in case of any conflict the sources of truth are prioritized in the above order. For example: In case of conflict between Sherlock rules vs Sherlock's historical decision, Sherlock criteria for issues must be considered the source of truth. In case of conflict between information in the README vs Sherlock rules, the README overrides Sherlock rules.

And yes private/discord messages does not matter:

Discord messages or DM screenshots are not considered sources of truth while judging an issue/escalation especially if they are conflicting with the contest README.

One thing to add while rereading this comment, shouldn't Sponsor final confirmation >>> contest readmi, exactly because some issue's severity can go overlooked, just like in the current case

nevillehuang

Afaik, Sponsor confirmation has never been the deciding factor for a finding. It has always been sherlock rules, contest details and the code itself. To note, this rules are introduce to ensure judging consistency in the first place.

I agree that this finding is valid, and it is unfair to watsons for not being rewarded. But it is also very unfair to me for potentially having my judging points penalized because the source of ambiguity is not because of me. I think all watsons know judging is not easy, and the rules revolving judging is extremely strict. Even one miss finding by me will heavily penalize me.

I leave this in the hands of @Evert0x @nasri136 and will respect any final outcome.

Czar102

First off, the status of whether the sponsor wants to fix the issue has nothing to do with its validity. Please don't use it as an argument of judgement.

Secondly, I understand @nevillehuang about the strictness of the judging rules and potential payout issues. Will see what can I do about it in this case. The mechanism shouldn't bias you towards maintaining the current judgement, so if you have concerns with that, we can try to resolve those rewarding formula issues in DMs.

About the issue, it is true that it is unfortunately conflicting with the README. I think we should rethink the way the docs describes the hierarchy of truth.

Audits exist not only to show outright mistakes in the code. Auditors' role is to show that some properties (that are important to parties using the code) are not enforced or fulfilled. They need to think in a broader sense than the sponsors did, in order to provide any more insight above the technical one. Auditors need to teach sponsors not only about issues, but also about the impacts. "Why is this property a problem?"

Sponsors can't know what wording to use not to exclude the bugs they don't care about. Because that's not their job. It's our job to understand their needs. We work for them to secure their code. We tell them what do they need.

What sponsors probably meant by "any reason causing the randomness request not to be fulfilled" is most likely "any external reason causing the randomness request not to be fulfilled". They didn't think it could be their contract causing this issue. I think some Watsons correctly identified what sponsors intended to convey and should be rewarded for submitting the issue.

Will alter the docs to account for this kind of situations.

Could you share your feedback on the above approach? @nevillehuang @PlamenTSV @gstoyanovbg

PlamenTSV

I am glad you understand the aspect of the README file overlooking the potential issues. It is not that they do not accept issues like this, it is the fact that they did not expect their contract to be at fault for them. I believe us watsons did in fact identify a valid problem, adhering both to the sherlock criteria and the sponsor's needs (we understand their confirmation is not a valid judgement, but it can be an argument that they did a mistake in the README), and hopefully I am speaking for everyone when I say I am glad we reached such a fair outcome. I am looking forward to seeing some docs changes for edge-case scenarios like this so they can be more easily resolved in the future. Thanks for the hard work!

nevillehuang

Hi @Czar102, I will share with you how I interpreted this while judging. In addition to the conflicted READ.ME, the emergencyWithdraw function exists in the first place to deal with such scenarios, that is why i deemed it as an accepted risk since funds can always be rescued by trusted admins.

While I also understand the other side of the equation, I will share with you what I think is the only possible fair outcome for both watsons auditing and judging. I also want to clarify that unfairness goes both ways, but arguably even more so for judging since judging has way stricter rules.

Hi all heres my input for the findings regarding 107 & 136. In my opinion, the only fair outcome for all parties for 107 & 136 is to validate the finding as medium severity and waive its potential impact on my judging status. After all, the finding has provided significant value to the sponsor given the fix implemented. But you can see from the current judging state that there will likely be less than 10 issues, so one missed issue by me can lead to heavy penalisation of my judging points or maybe even lead me to not make the minimum 80% recall threshold. I have included this in part of my growing list of suggestion for improvement to sherlock, that is we possibly need a period of 24-48 hour for sponsor to clarify/edit any doubts regarding contest details. Lead judge/head of judging can facilitate in this. Of course this is just my opinion, I leave the rest up to the temporary head of judging and sherlock

gstoyanovbg

@Czar102, I agree with everything you've said, and I share your philosophy on the work of auditors. Regarding the rules for judging, in my opinion, they cannot cover all possible cases, especially those that have not arisen before. They should evolve over time, and in the event of a situation, the Sherlock team should be able to make a decision so that there are no affected judges and auditors. @nevillehuang, I still believe this is a high severity issue, but at the same time, it is not fair to be punished because you acted according to the defined rules. I hope a solution can be found.

Czar102

the emergencyWithdraw function exists in the first place to deal with such scenarios

Nevertheless, the contract doesn't function correctly. I think that because it is possible to rescue funds, it can be a medium severity issue. Players who were supposed to win in a game don't get their funds, but the EV doesn't change. The result is never uncovered. Hence medium.

Because judges who approached this issue "by the book" would lose out on this outcome, this issue and duplicates will not count towards the judging contest reward calculation.

@nevillehuang @nasri136 Please let me know whether, according to the above approach, #72 should be considered valid or not. It seems it presents another issue.

Czar102

Result: Medium Has duplicates

The issue and duplicates will not be counted towards the judging payout.

sherlock-admin2

Escalations have been resolved successfully!

Escalation status:

gstoyanovbg

@Czar102, allow me to disagree with the severity of the report. I'm not sure if I have the right to dispute it after the escalation is resolved, but here are my arguments:

  1. I agree that the funds can be rescued using emergencyWithdraw(). However, the funds locked in one iteration of the game are insignificant compared to the indirect losses from such an event. The success of luck-based games is directly tied to the trust of the players in the game creators. If the game is interrupted at a crucial moment (which is very likely), the affected players will certainly question the fairness of the game. Can they be sure that this is not intentionally caused for someone not to win the prize? The damage to the brand's reputation will be irreversible. In addition to the missed profits from hundreds of future iterations of the game, developers will incur losses from the funds invested in development.

  2. I mentioned earlier that when we talk about financial losses, we should also consider the losses for users from gas fees (may have thousands of players). We all know what the fees on the Ethereum mainnet can be, especially in moments of network congestion. For a player whose agent is wounded, it may be extremely important to heal it regardless of the fee paid. When funds are withdrawn through emergencyWithdraw() and returned to the players, they should be compensated for gas fees. These funds must be paid either by Looksrare or the players. In both cases, someone loses.

  3. The time lost by players to play a game that suddenly stops and cannot continue should also be taken into account. Even if the game starts again from the beginning and everything is fine, it cannot start from the same state it was interrupted. A player may have been in a position where they were likely to win a prize, but they probably won't be compensated for it. Even if they are, it will be at the protocol's expense.

Considering 1), 2), and 3), my position is that there are much larger losses for each party than just the funds locked in the contract at the time of the interruption.

Czar102

Can they be sure that this is not intentionally caused for someone not to win the prize?

If that's the case, this could have been a high severity issue. I don't think that the report mentions such a scenario though.

The damage to the brand's reputation will be irreversible.

That's true, but that is not a high severity vulnerability. A high severity vulnerability is when there is a direct and very probable loss of funds, which is not the case here.

These funds must be paid either by Looksrare or the players. In both cases, someone loses.

The gas costs are out of scope here. The fact that the user plays the game is a "value gotten" for the gas. Anyway, even if, there would be no high severity impact because of gas.

A player may have been in a position where they were likely to win a prize, but they probably won't be compensated for it.

We don't know that. Maybe the protocol would distribute the rewards proportionally to the EV of the players in a given position given an optimal strategy?

This is why I chose a Medium severity for this issue.

gstoyanovbg

If that's the case, this could have been a high severity issue. I don't think that the report mentions such a scenario though.

Me and you know that this is not true, but for people which are not solidity developers / auditors it is not clear and creates reasonable doubt about the fairness of the game and its creators

That's true, but that is not a high severity vulnerability. A high severity vulnerability is when there is a direct and very probable loss of funds, which is not the case here.

I agree there is no direct loss of funds but we have an issue that breaks core contract functionality, rendering the protocol/contract useless + indirect loss of funds on a large scale.

We don't know that. Maybe the protocol would distribute the rewards proportionally to the EV of the players in a given position given an optimal strategy?

That would be the right decision. However, there is no way to do it in a good enough manner because there is no way to prove which player had what financial resources for the game. This is a very important variable for the potential mathematical model. For example, two agents who have survived until round X and have been healed three times will have equal weight if the game is interrupted at that moment. However, the player behind the first may no longer have the financial ability to heal the agent, while the second may be able to heal it three more times without any problem.

Czar102

Me and you know that this is not true, but for people which are not solidity developers / auditors it is not clear and creates reasonable doubt about the fairness of the game and its creators

This is exactly why we have this job. We need to tell them. And this impact doesn't seem to be the case here.

However, there is no way to do it in a good enough manner because there is no way to prove which player had what financial resources for the game.

Yes, but I feel that's an insufficient impact to consider a high severity impact. In the end, they might make another contract and continue the game ;) Ofc that would be costly but the loss of funds is extremely limited here. This is why it's Medium.

0xhiroshi

https://github.com/LooksRare/contracts-infiltration/pull/165

SergeKireev

Fix LGTM

2023-10-looksrare-judging's People

Contributors

rcstanciu avatar sherlock-admin avatar sherlock-admin2 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

2023-10-looksrare-judging's Issues

mstpr-brainbot - If escapes makes an agent winner while there are healing agents the healing agents LOOKS are lost

mstpr-brainbot

medium

If escapes makes an agent winner while there are healing agents the healing agents LOOKS are lost

Summary

When escapes happen in a level to make some agent winner (only active agent in the game) while there are some healing agents the game will be over and the healing agents lost their heal cost.

Vulnerability Detail

Assume there are 50 active agents alive in the game and 15 healing agents. Assume that Alice is the owner of the all 50 agents and she sees that there are 15 healing agents. Those healers already paid the equivalent LOOKS to have a chance to be alive in the next round. Alice can take advantage of this situation and escapes her 49 agents making the game over. Those 15 healing agents LOOKS are sent to Alice and their heal didn't do what it supposed to.

Note that Alice having ownership of the last 50 active agents is not a must. This scenario can happen without having such a power but would be more unlikely because of the nature of competition.

Impact

As stated above the scenario is possible and there is a loss of funds possibility for all those healers. Considering the possibility of such scenarios happening and the funds lost I will label this as medium

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L716-L796
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L801-L916
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L656-L704

Tool used

Manual Review

Recommendation

Do not let the game to be over when there are pending heal requests to have a fair competition for the players

Duplicate of #98

Krace - The buyer of the winner's Infiltration NFT may face front-running risks, as the winner could claim the rewards before the Infiltration NFT is traded

Krace

medium

The buyer of the winner's Infiltration NFT may face front-running risks, as the winner could claim the rewards before the Infiltration NFT is traded

Summary

If the grand prize winner lists the winning Infiltration NFT on the secondary market and initiates their claiming transaction before the trade transaction occurs, the NFT buyer could potentially incur financial losses.

Vulnerability Detail

As implemented in Infiltration.transferFrom, only Infiltration NFTs in the "Active" and "Wounded" states can be transferred or traded.

    /**
     * @notice Only active and wounded agents are allowed to be transferred or traded.
     * @param from The current owner of the token.
     * @param to The new owner of the token.
     * @param tokenId The token ID.
     */
    function transferFrom(address from, address to, uint256 tokenId) public payable override {
        AgentStatus status = agents[agentIndex(tokenId)].status;
        if (status > AgentStatus.Wounded) {
            revert InvalidAgentStatus(tokenId, status);
        }
        super.transferFrom(from, to, tokenId);
    }

After a game ends, only the first-place player(the winenr) remains active, enabling the winner's NFT to be traded.
Considering the following scenario:

  1. Alice wins the game and lists the NFT on the secondary market with less moeny than the prizes(including grand prize and secondary prize).
  2. Bob decides to purchase the NFT, recognizing that it is profitable to trade and claim the prizes associated with it.
  3. Alice monitors the mempool and submits a claimGrandPrize transaction just before Bob’s purchase transaction.
  4. Bob receives the NFT and calls to claimGrandPrize but this transaction would revert as the prizes have already been claimed by Alice.
    Consequently, Alice receives the prizes and the the amount of the transaction, while Bob is left with an empty NFT and no prizes.

To demonstrate this scenario, you can add the following test to contracts-infiltration/test/foundry/Infiltration.claimGrandPrize.t.sol. For the sake of simplicity, Alice will only call claimGrandPrize before transferring. It's important to note that this scenario can arise not only from the grand prize but also from the secondary prizes.

Run with forge test --match-test test_transferWinner -vv, it will revert because the grand prize has been clamied by Alice.

diff --git a/contracts-infiltration/test/foundry/Infiltration.claimGrandPrize.t.sol b/contracts-infiltration/test/foundry/Infiltration.claimGrandPrize.t.sol
index 18d926e..3796b20 100644
--- a/contracts-infiltration/test/foundry/Infiltration.claimGrandPrize.t.sol
+++ b/contracts-infiltration/test/foundry/Infiltration.claimGrandPrize.t.sol
@@ -7,6 +7,7 @@ import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2
 import {IInfiltration} from "../../contracts/interfaces/IInfiltration.sol";

 import {TestHelpers} from "./TestHelpers.sol";
+import {IERC721A} from "erc721a/contracts/IERC721A.sol";

 contract Infiltration_ClaimGrandPrize_Test is TestHelpers {
     function setUp() public {
@@ -15,6 +16,22 @@ contract Infiltration_ClaimGrandPrize_Test is TestHelpers {
         _setMintPeriod();
     }

+    function test_transferWinner() public {
+        _downTo1ActiveAgent();
+
+        IInfiltration.Agent memory agent = infiltration.getAgent(1);
+        address winner = _ownerOf(agent.agentId);
+
+        vm.startPrank(winner);
+        infiltration.claimGrandPrize();
+
+        IERC721A(address(infiltration)).transferFrom(winner, user1, agent.agentId);
+
+        vm.startPrank(user1);
+        infiltration.claimGrandPrize();
+
+    }
+
     function test_claimGrandPrize() public {
         _downTo1ActiveAgent();

Impact

The buyer of the winner's Infiltration NFT will lose all their funds and receive only the claimed NFT.

Code Snippet

Infiltration.sol#L656-L672
Infiltration.sol#L677-L711

Tool used

Manual Review, Foundry

Recommendation

To ensure the NFT's security, consider either burning the winner's NFT or marking its status as "Dead" after it has been claimed for both the grand prize and secondary prizes. This action will help prevent any further transactions or potential issues with the NFT.

Duplicate of #56

AlexCzm - Precision loss in `healProbability` due to rounding error

AlexCzm

medium

Precision loss in healProbability due to rounding error

Summary

healProbability is subject to slightly precision loss due to rounding error.

Vulnerability Detail

healProbability is used to determine the probability of a wounded agent to be healed. Function output is compared with a Chainlink VRF number. If heal probability is smaller or equal than VRF number agent is healed and killed otherwise.
As we can see the heal probability is an important aspect of the game dictating whether or not an agent remains in the game.

In current implementation heal probability is calculated as following:

healProb = (ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD * 99 - 80) * PROBABILITY_PRECISION / 
                       ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE - 
                       (blocksDelay * 19 * PROBABILITY_PRECISION  /
                       ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE

Adding the numbers for a better visualization:

 healProb = (48 * 99 - 80) * 100_000_000 / 47 - 
                       blocksDelay * 19 * 100_000_000 / 47

This formula is composed of two main parts that are subtracted from each other. Both parts are divided by the same number, which is 47 (judging from mainnet deployment script values and docs provided). Even if a PROBABILITY_PRECISION was used to avoid or minimize the precision loss it is not enough.
Instead of subtracting two fractions with same divisor (47) we subtract the two terms first and divide the result by 47 leads to slightly different results in 3 out of 48 blocksDelay values:

Add the following code in Infiltration#healProbability.t.sol file and execute it with forge test --mt test_healProbability_RoundingError -vvv

    uint256 private constant PROBABILITY_PRECISION = 100_000_000;
    uint256 private constant ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE = 47;
    function getHealProbabilityWithNoPrecisionLoss(uint256 blocksDelay) pure private returns(uint256 y) {
        y = ((ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD * 99 - 80) * PROBABILITY_PRECISION - 
                blocksDelay * 19 * PROBABILITY_PRECISION
            ) / 
                ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;
    }
    function test_healProbability_CheckRoundingError() public {
        assertEq(infiltration.healProbability(1), getHealProbabilityWithNoPrecisionLoss(1));
        assertEq(infiltration.healProbability(2), getHealProbabilityWithNoPrecisionLoss(2));
        assertEq(infiltration.healProbability(3), getHealProbabilityWithNoPrecisionLoss(3));
        assertEq(infiltration.healProbability(4), getHealProbabilityWithNoPrecisionLoss(4));
        assertEq(infiltration.healProbability(5), getHealProbabilityWithNoPrecisionLoss(5));
        assertEq(infiltration.healProbability(6), getHealProbabilityWithNoPrecisionLoss(6));
        assertEq(infiltration.healProbability(7), getHealProbabilityWithNoPrecisionLoss(7));
        assertEq(infiltration.healProbability(8), getHealProbabilityWithNoPrecisionLoss(8));
        assertEq(infiltration.healProbability(9), getHealProbabilityWithNoPrecisionLoss(9));
        assertEq(infiltration.healProbability(10), getHealProbabilityWithNoPrecisionLoss(10));
        assertEq(infiltration.healProbability(11), getHealProbabilityWithNoPrecisionLoss(11));
        assertEq(infiltration.healProbability(12), getHealProbabilityWithNoPrecisionLoss(12));
        assertEq(infiltration.healProbability(13), getHealProbabilityWithNoPrecisionLoss(13));
        assertEq(infiltration.healProbability(14), getHealProbabilityWithNoPrecisionLoss(14));
        assertEq(infiltration.healProbability(15), getHealProbabilityWithNoPrecisionLoss(15));
        assertEq(infiltration.healProbability(16), getHealProbabilityWithNoPrecisionLoss(16));
        assertEq(infiltration.healProbability(17), getHealProbabilityWithNoPrecisionLoss(17));
        assertEq(infiltration.healProbability(18), getHealProbabilityWithNoPrecisionLoss(18));
        assertEq(infiltration.healProbability(19), getHealProbabilityWithNoPrecisionLoss(19));
        assertEq(infiltration.healProbability(20), getHealProbabilityWithNoPrecisionLoss(20));
        assertEq(infiltration.healProbability(21), getHealProbabilityWithNoPrecisionLoss(21));
        assertEq(infiltration.healProbability(22), getHealProbabilityWithNoPrecisionLoss(22));
        assertEq(infiltration.healProbability(23), getHealProbabilityWithNoPrecisionLoss(23));
        assertEq(infiltration.healProbability(24), getHealProbabilityWithNoPrecisionLoss(24));
        assertEq(infiltration.healProbability(25), getHealProbabilityWithNoPrecisionLoss(25));
        assertEq(infiltration.healProbability(26), getHealProbabilityWithNoPrecisionLoss(26));
        assertEq(infiltration.healProbability(27), getHealProbabilityWithNoPrecisionLoss(27));
        assertEq(infiltration.healProbability(28), getHealProbabilityWithNoPrecisionLoss(28));
        assertEq(infiltration.healProbability(29), getHealProbabilityWithNoPrecisionLoss(29));
        assertEq(infiltration.healProbability(30), getHealProbabilityWithNoPrecisionLoss(30));
        assertEq(infiltration.healProbability(31), getHealProbabilityWithNoPrecisionLoss(31));
        assertEq(infiltration.healProbability(32), getHealProbabilityWithNoPrecisionLoss(32));
        assertEq(infiltration.healProbability(33), getHealProbabilityWithNoPrecisionLoss(33));
        assertEq(infiltration.healProbability(34), getHealProbabilityWithNoPrecisionLoss(34));
        assertEq(infiltration.healProbability(35), getHealProbabilityWithNoPrecisionLoss(35));
        assertEq(infiltration.healProbability(36), getHealProbabilityWithNoPrecisionLoss(36));
        assertEq(infiltration.healProbability(37), getHealProbabilityWithNoPrecisionLoss(37));
        assertEq(infiltration.healProbability(38), getHealProbabilityWithNoPrecisionLoss(38));
        assertEq(infiltration.healProbability(39), getHealProbabilityWithNoPrecisionLoss(39));
        assertEq(infiltration.healProbability(40), getHealProbabilityWithNoPrecisionLoss(40));
        assertEq(infiltration.healProbability(41), getHealProbabilityWithNoPrecisionLoss(41));
        assertEq(infiltration.healProbability(42), getHealProbabilityWithNoPrecisionLoss(42));
        assertEq(infiltration.healProbability(43), getHealProbabilityWithNoPrecisionLoss(43));
        assertEq(infiltration.healProbability(44), getHealProbabilityWithNoPrecisionLoss(44));
        assertEq(infiltration.healProbability(45), getHealProbabilityWithNoPrecisionLoss(45));
        assertEq(infiltration.healProbability(46), getHealProbabilityWithNoPrecisionLoss(46));
        assertEq(infiltration.healProbability(47), getHealProbabilityWithNoPrecisionLoss(47));
        assertEq(infiltration.healProbability(48), getHealProbabilityWithNoPrecisionLoss(48));
    }

Below are only the rounding error cases:

    ├─ [971] Infiltration::healProbability(12) [staticcall]
       └─  9455319149 [9.455e9]
    ├─ emit log(: Error: a == b not satisfied [uint])
    ├─ emit log_named_uint(key:       Left, val: 9455319149 [9.455e9])
    ├─ emit log_named_uint(key:      Right, val: 9455319148 [9.455e9])
    ├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001) 
       └─  ()
...

    ├─ [971] Infiltration::healProbability(24) [staticcall]
       └─  8970212766 [8.97e9]
    ├─ emit log(: Error: a == b not satisfied [uint])
    ├─ emit log_named_uint(key:       Left, val: 8970212766 [8.97e9])
    ├─ emit log_named_uint(key:      Right, val: 8970212765 [8.97e9])
    ├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001) 
       └─  ()

...

    ├─ [971] Infiltration::healProbability(36) [staticcall]
       └─  8485106383 [8.485e9]
    ├─ emit log(: Error: a == b not satisfied [uint])
    ├─ emit log_named_uint(key:       Left, val: 8485106383 [8.485e9])
    ├─ emit log_named_uint(key:      Right, val: 8485106382 [8.485e9])
    ├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001) 
       └─  ()

Impact

The game's outcome might sometimes be incorrect.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L379-L381

        HEAL_PROBABILITY_MINUEND =
            ((ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD * 99 - 80) * PROBABILITY_PRECISION) /
            ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1019-L1028

    function healProbability(uint256 healingBlocksDelay) public view returns (uint256 y) {
        if (healingBlocksDelay == 0 || healingBlocksDelay > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD) {
            revert InvalidHealingBlocksDelay();
        }


        y =
            HEAL_PROBABILITY_MINUEND -
            ((healingBlocksDelay * 19) * PROBABILITY_PRECISION) /
            ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;
    }

Tool used

Manual Review, Foundry

Recommendation

Update the healProbability to subtract the two parts first and divide the result by 47 in the end.

sil3th - Access Control Issue in the _swap Function

sil3th

high

Access Control Issue in the _swap Function

Summary

Unauthorized users can call the _swap function.

Vulnerability Detail

The _swap function in the code snippet is missing explicit access control checks. It is essential to ensure that only authorized users can call this function. Without proper access control, there is a risk that unauthorized users being able to invoke the _swap function.

Impact

Malicious actor can do a swap.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L753-L759

Tool used

Manual Review

Recommendation

To mitigate this issue, it is recommended to add access control checks to the _swap function. These checks should verify whether the caller of the function (msg.sender) is the one who is calling the function and revert if not.

0xruffbuff.eth - Reentrancy Vulnerability in InfiltrationPeriphery Contract

0xruffbuff.eth

high

Reentrancy Vulnerability in InfiltrationPeriphery Contract

Summary

The InfiltrationPeriphery contract lacks a reentrancy guard which exposes the heal function to potential reentrancy attacks. This could lead to unexpected behavior and potentially loss of funds.

Vulnerability Detail

In the heal function, after the amountIn is calculated through SWAP_ROUTER.exactOutputSingle, funds are returned back to the contract through a call to SWAP_ROUTER.refundETH() and then forwarded to the sender via _transferETHAndWrapIfFailWithGasLimit. An external contract could have the ability to call the heal function again before these operations complete, leading to incorrect calculations and transfers of funds. The lack of a reentrancy guard allows for potential nested calls to the heal function, which is a classic example of a reentrancy vulnerability.

Impact

If exploited, this vulnerability could lead to incorrect state, unexpected behavior, and potentially loss of funds. It could disrupt the normal functioning of the contract and could be used by an attacker to drain funds from the contract under certain conditions.

Code Snippet

The vulnerability is located in the heal function of the InfiltrationPeriphery contract:
Code location

function heal(uint256[] calldata agentIds) external payable {
    uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);
    IV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({
        tokenIn: WETH,
        tokenOut: LOOKS,
        fee: POOL_FEE,
        recipient: address(this),
        amountOut: costToHealInLOOKS,
        amountInMaximum: msg.value,
        sqrtPriceLimitX96: 0
    });
    uint256 amountIn = SWAP_ROUTER.exactOutputSingle{value: msg.value}(params);
    IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);
    INFILTRATION.heal(agentIds);
    if (msg.value > amountIn) {
        SWAP_ROUTER.refundETH();
        unchecked {
            _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, msg.value - amountIn, gasleft());
        }
    }
}

Tool used

Manual Review

Recommendation

Implement a reentrancy guard to prevent nested calls to the heal function. One common approach is to use the nonReentrant modifier from the OpenZeppelin library which ensures that a function cannot be re-entered before the initial call has completed.

0xruffbuff.eth - Lack of Return Value Check

0xruffbuff.eth

medium

Lack of Return Value Check

Summary

The return value of _executeERC20DirectTransfer is not checked, which could lead to undetected failures in token transfers.

Vulnerability Detail

The function _healRequestFulfilled (line 1368) invokes _executeERC20DirectTransfer to transfer tokens. However, the return value of _executeERC20DirectTransfer is not checked within the _healRequestFulfilled function. The ERC-20 standard specifies that the transfer function should return a boolean value indicating success or failure, but this return value is ignored in the given code snippet (line 1371). Failing to check this return value could mean that a failed transfer goes unnoticed, leading to an incorrect state within the smart contract or potential loss of funds. This kind of oversight could be exploited by an attacker, especially in cases where the state of the contract changes based on the success of the token transfer.

Here's the relevant code snippet from the smart contract:

_executeERC20DirectTransfer(LOOKS, 0x000000000000000000000000000000000000dEaD, _costToHeal(lastHealCount) / 4);

In this line, tokens are being transferred to a designated address, but the success of the transfer is not being verified. This could potentially lead to an incorrect contract state if, for example, the transfer fails but the contract proceeds as if it were successful. This lack of error handling and verification is a significant issue and could lead to unexpected behavior or even exploitation in adversarial conditions.

Impact

Failure to check the return value could lead to loss of funds or incorrect contract behavior.

Code Snippet

Code location

// @audit-issue: Lack of return value check can lead to undetected failures.
_executeERC20DirectTransfer(LOOKS, 0x000000000000000000000000000000000000dEaD, _costToHeal(lastHealCount) / 4);

Tool used

Manual Review

Recommendation

Check the return value of _executeERC20DirectTransfer and handle any errors appropriately to ensure that the token transfer was successful.
Consider reverting the transaction or logging an error event if the function call fails.

pks_ - Unlimited account type may leading to out of gas error

pks_

high

Unlimited account type may leading to out of gas error

Summary

Unlimited account type may leading to out of gas error.

Vulnerability Detail

Infiltration#_transferETHAndWrapIfFailWithGasLimit function is used by several places to send ETH token to msg.sender, like claimGrandPrize, claimSecondaryPrizes and escape functions. However, there is no limit account type, it has possibility of leading to out of gas error if the winner is contract account and the contract has some gas consumption action when receive the ETH token.

When the winner is contract account like multi-sign wallet, it's very possible to send price failed, but the protocol have no corresponding mechanism to cover such case.

Besides, the call inside _transferETHAndWrapIfFailWithGasLimit may failed due to 1/64 rule because it use gasleft as gaslimit without considering 1/64 rule, see EIP-150 and evm.codes.

The following PoC shows that if the winner is contract account and the contract receive function do some gas consumption action, it will lead to out of gas error.

contract LowLevelWETH {
    address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    function _transferETHAndWrapIfFailWithGasLimit(
        address _WETH,
        address _to,
        uint256 _amount,
        uint256 _gasLimit
    ) external {
        bool status;

        assembly {
            status := call(_gasLimit, _to, _amount, 0, 0, 0, 0)
        }

        if (!status) {
            WETH9(_WETH).deposit{value: _amount}();
            WETH9(_WETH).transfer(_to, _amount);
        }
    }

    receive() external payable {
        
    }

    fallback() external payable {
        
    }
}


// forge test --match-path test/pocs/Demo.t.sol -vvv
contract DemotTest is Test {

    Transfer transfer;
    constructor(){
        transfer = new Transfer();
    }

    function testGas() public {
        payable(address(transfer)).transfer(10 ether);
        this.entryPoint{gas: 1000000}();
    }

    function entryPoint() public {
        console2.log("before gasleft: ", gasleft());
        // try transfer.gasCall() {
            
        // } catch  {
            
        // }
        transfer.gasCall();
        console2.log("after gasleft: ", gasleft());
    }
}

contract Receiver {
    event PushFinised();
    constructor() {
        
    }

    uint256[] myArr;
    receive() external payable {
        for (uint256 i = 0; i < 100; i++) {
            myArr.push(i);
        }
        // this line will never reached because the out of gas issue
        emit PushFinised();
    }
}


contract Transfer {

    LowLevelWETH lowLevelWETH = new LowLevelWETH();

    function gasCall() external {
        this.intermediate();
    }

    function intermediate() public {
        payable(address(lowLevelWETH)).transfer(1 ether);
        Receiver receiver = new Receiver();
        lowLevelWETH._transferETHAndWrapIfFailWithGasLimit(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, address(receiver), 1, gasleft());
    }

    receive() external payable {
        
    }

    fallback() external payable {
        
    }
}

Results:

[FAIL. Reason: EvmError: Revert] testGas() (gas: 957062)
Logs:
  before gasleft:  999817

Traces:
  [957062] DemotTest::testGas() 
    ├─ [55] Transfer::receive() 
    │   └─ ← ()
    ├─ [944984] DemotTest::entryPoint() 
    │   ├─ [0] console::log(before gasleft: , 999817 [9.998e5]) [staticcall]
    │   │   └─ ← ()
    │   ├─ [941187] Transfer::gasCall() 
    │   │   ├─ [940717] Transfer::intermediate() 
    │   │   │   ├─ [55] LowLevelWETH::receive() 
    │   │   │   │   └─ ← ()
    │   │   │   ├─ [45099] → new Receiver@0x037eDa3aDB1198021A9b2e88C22B464fD38db3f3
    │   │   │   │   └─ ← 225 bytes of code
    │   │   │   ├─ [851193] LowLevelWETH::_transferETHAndWrapIfFailWithGasLimit(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, Receiver: [0x037eDa3aDB1198021A9b2e88C22B464fD38db3f3], 1, 875833 [8.758e5]) 
    │   │   │   │   ├─ [832951] Receiver::receive() 
    │   │   │   │   │   └─ ← "EvmError: OutOfGas"
    │   │   │   │   └─ ← "EvmError: Revert"
    │   │   │   └─ ← "EvmError: Revert"
    │   │   └─ ← "EvmError: Revert"
    │   └─ ← "EvmError: Revert"
    └─ ← "EvmError: Revert"

Impact

out of gas error may happen when the winner is contract account.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L521

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L565

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L669

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L693

Tool used

Manual Review

Recommendation

  1. Add try - catch block to _transferETHAndWrapIfFailWithGasLimit function to make sure the claim action can be executed successfully.
  2. Modify the required amount of gasleft to gasLimit + any amount of gas spent before reaching the call(), then multiply it by 32/30 to mitigate the 1/64 rule (+ some margin of safety maybe).

sil3th - Precision Loss Due to Non-Exact Multiples in Division

sil3th

medium

Precision Loss Due to Non-Exact Multiples in Division

Summary

When prizePool and currentRoundAgentsAlive are not exact multiples of each other, the division operation will lead to precision loss. Solidity performs integer division by truncating the fractional part, meaning it discards the remainder. This behavior can result in incorrect calculations when the intention is to maintain the fractional part.

Vulnerability Detail

Explained in the POC Below

`pragma solidity ^0.8.0;

contract PrecisionLossExample {
uint256 public prizePool;
uint256 public currentRoundAgentsAlive;
uint256 public totalEscapeValue;

constructor(uint256 _prizePool, uint256 _currentRoundAgentsAlive) {
prizePool = _prizePool;
currentRoundAgentsAlive = _currentRoundAgentsAlive;
totalEscapeValue = prizePool / currentRoundAgentsAlive;
}

function calculateTotalEscapeValue() public view returns (uint256) {
return totalEscapeValue;
}
}

`
In this example, the prizePool is set to 100, and currentRoundAgentsAlive is set to 30. When we deploy the contract and calculate totalEscapeValue, we can observe that precision loss occurs due to integer division:

`// Deployment
PrecisionLossExample example = new PrecisionLossExample(100, 30);

// Retrieve the calculated totalEscapeValue
uint256 result = example.calculateTotalEscapeValue(); // result will be 3
`
In this case, the result of the division is 3, which is the integer part of the result. The fractional part is discarded due to integer division, resulting in precision loss.

Impact

Precision loss can lead to inaccurate calculation of totalEscapeValue.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L740C31-L740C31

Tool used

Manual Review

Recommendation

Scale Up Values Before Division: Multiply both prizePool and currentRoundAgentsAlive by a common scaling factor before performing the division. This scaling factor should be a power of 10 to ensure precision is maintained.

Vagner - `InfiltrationPeriphery.sol` is useless since the swap router parameters are wrong and the functions will revert all the time

Vagner

high

InfiltrationPeriphery.sol is useless since the swap router parameters are wrong and the functions will revert all the time

Summary

The two functions used in the InfiltrationPeriphery.sol will revert all the time because the parameters used for the swap router are wrong.

Vulnerability Detail

InfiltrationPeriphery.sol has two functions, costToHeal used to see how much it will cost to heal an agent in WETH, by calling QuoterV2 and heal that is used to heal an agent by using WETH, swapped by using the UniswapV3 router. The problem relies in the fact that the parameters used in the heal function to call exactOutputSingle in the swap router are wrong. In the InfiltrationPeriphery.sol contract the parameters used look like this
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/interfaces/IV3SwapRouter.sol#L7-L15
while the one from the UniswapV3 docs look like this
image
as you can see, it is missing the deadline parameters. Because of that, anytime the contract will try to call exactOutputSingle with those parameters the call will revert, because the decoding will expect a bigger number of parameters, and it will not find the deadline one to be checked here
https://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/SwapRouter.sol#L207
making the function, and pretty much the whole contract unusable.

Impact

Impact is a high one since the contract is rendered unusable.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L48-L56

Tool used

Manual Review

Recommendation

Add the deadline parameter, so the function will not revert.

sandNallani - A game can be started with just one player, which should be an invalid game

sandNallani

false

A game can be started with just one player, which should be an invalid game

Summary

The game can start with just one agent as long as the mint end time has passed.
This is possible because there's no restriction set on the minimum number of agents to start a valid game.

Vulnerability Detail

The purpose of the game is to get a random word to decide the fate of the agents and reducing the number of active agents either by killing or wounding agents until only one agent survives.
However, if only one agent is minted, and the the mint end time has passed. If the game is started, it will result in the sole agent being the winner.

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L501

Impact

This results in an invalid game where the sole agent takes the winnings.

Code Snippet

    function test_startGame_RevertIf_GameAlreadyBegun_1() public {
        vm.warp(_mintStart());

        uint160 startingUser = 11;

        for (uint160 i = startingUser; i < 1 + startingUser; i++) {
            vm.deal(address(i), PRICE * 1);
            vm.prank(address(i));
            infiltration.mint{value: PRICE * 1}({quantity: 1});
        }
        vm.prank(owner);
        vm.warp(block.timestamp+ 1 days);
   

        infiltration.startGame();

    }
    ```
## Tool used
Foundry

## Recommendation
 Set a minimum threshold for the number of players and ensuring that it is surpassed before starting the game.   

cergyk - A participant with enough active agents can force win for his wounded agents

cergyk

high

A participant with enough active agents can force win for his wounded agents

Summary

Infiltration contract decides the game is over when only one Active agent is left. The assumption is that the game will go through a round which is initialized with number of agents < NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS, such as all remaining wounded agents are killed at that point.

However if a participant has enough agents to escape, it may not be the case, and the participant can skip the killing of wounded agents and allow these agents to claim secondary prizes.

Vulnerability Detail

Let's examine the following scenario:

Prerequisites

  • NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS = 50
  • Alice holds 20 active agents (ranked 31 to 51) and 20 wounded agents (ranked 1 to 20)
  • Bob holds 1 active agent and 10 healing agents (ranked 20 to 31)
    (Rankings are consecutive for simplicity of the example)

Steps

1/ Alice escapes all of her active agents, freezing the ranks of all of the remaining alive agents.

Note that since the game is over, Bob cannot call startNewRound() to initiate the killing of Alice's wounded agents:
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L590-L648

2/ With her wounded agents, Alice can claim Grand prize, and top secondary prizes,
3/ Whereas with healing and last active agent, Bob can only claim lowest secondary prizes

Impact

Prizes are unfairly distributed, this is a loss of funds for honest participants

Code Snippet

Tool used

Manual Review

Recommendation

Enforce the cleaning of wounded participants when game is over to claim grand prize

Duplicate of #98

Beosin - Algorithm error

Beosin

false

Algorithm error


name: Algorithm error
about: These are the audit items that end up in the report
title: "Algorithm error"
labels: "Medium"
assignees: "Beosin"


Summary

Inconsistency in calculations related to the HEAL_PROBABILITY_MINUEND parameter

Vulnerability Detail

Inconsistency between the parameters used in the initialization HEAL_PROBABILITY_MINUEND and subsequent calculations in the healProbability function.
The algorithm for calculating HEAL_PROBABILITY_MINUEND is *99-80. the calculation in HEALProbability is *19 (which is 99-80) .

Impact

The result of the healProbability function calculation is large, affecting the token destruction in _healRequestFulfilled, and the return value deadAgentsCount is not increased. The end of the game was delayed.

Code Snippet

Infiltration.sol#L379-381
Infiltration.sol#L1019-1028

Tool used

Manual Review

Recommendation

It is recommended to modify the HEAL_PROBABILITY_MINUEND parameter or the calculation in the healProbability function.

cergyk - Winning agent id may be uninitialized when game is over, locking grand prize

cergyk

high

Winning agent id may be uninitialized when game is over, locking grand prize

Summary

In the Infiltration contract, the agents mapping holds all of the agents structs, and encodes the ranking of the agents (used to determine prizes at the end of the game).

This mapping records are lazily initialized when two agents are swapped (an agent is either killed or escapes):

  • The removed agent goes to the end of currently alive agents array with the status Killed/Escaped and its agentId is set
  • The last agent of the currently alive agents array is put in place of the previously removed agent and its agentId is set

This is the only moment when the agentId of an agent record is set.

This means that if the first agent in the array ends up never being swapped, it keeps its agentId as zero, and the grand prize is unclaimable.

Vulnerability Detail

We can see in the implementation of claimGrandPrize that:
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L658

The field Agent.agentId of the struct is used to determine if the caller can claim. Since the id is zero, and it is and invalid id for an agent, there is no owner for it and the condition:

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1666-L1670

Always reverts.

Impact

The grand prize ends up locked/unclaimable by the winner

Code Snippet

Tool used

Manual Review

Recommendation

In claimGrandPrize use 1 as the default if agents[1].agentId == 0:

function claimGrandPrize() external nonReentrant {
    _assertGameOver();
    uint256 agentId = agents[1].agentId;
+   if (agentId == 0)
+       agentId = 1;
    _assertAgentOwnership(agentId);

    uint256 prizePool = gameInfo.prizePool;

    if (prizePool == 0) {
        revert NothingToClaim();
    }

    gameInfo.prizePool = 0;

    _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, prizePool, gasleft());

    emit PrizeClaimed(agentId, address(0), prizePool);
}

mstpr-brainbot - If game has only 1 participant then there will be no winner and ETH will be stuck in the game contract

mstpr-brainbot

medium

If game has only 1 participant then there will be no winner and ETH will be stuck in the game contract

Summary

The game can commence under two conditions:

1- Once the total supply is reached.
2- When the mintEnd time is reached.
If the game has only one participant, then as soon as the game starts, the first fulfillRandomWords request from Chainlink will eliminate the sole agent, reducing the number of alive agents to zero. With no surviving agents, there will be no prize to claim. Even though the only claimable prize would have been the ETH deposited by that lone agent, the agent's ETH will not be refunded.

Vulnerability Detail

Assume there are many games played at the same time and one of the games has no participants and only 1 participant which is Alice and she deposited some ETH to bought her only agent. When the game starts because block.timestamp > mintEnd the very first chainlink call will kill the only agent which is the Alice's agent. This will make the alive agents as 0 and the ETH Alice deposited is stucked at the game contract. Alice can not call the claimGrandPrize to get her ETH back because of this check:
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L657

Impact

Although it is not very likely to begin a game with only 1 agent it is still possible especially if there can be many games played simultaneously. Also, if such case happens owner can use the emergency withdraw to reimburse Alice's ETH. Considering all of that I label this as medium since it requires an additional tx from the owner and also it spends unnecessary LINK tokens when it calls the fulfill request. So there are some funds lost for the owner and the functionality is not fully correct (do not start the game with only 1 agent)

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L499-L523
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1096-L1249

Tool used

Manual Review

Recommendation

Do not let a game start with only 1 agent

Duplicate of #65

cu5t0mPe0 - The game can start during the mint period

cu5t0mPe0

medium

The game can start during the mint period

Summary

Due to lax checking, it is possible for the game to start during the mint period

Vulnerability Detail

In the mint function, the mint period end time is mintEnd(include)

    /**
     * @inheritdoc IInfiltration
     */
    function mint(uint256 quantity) external payable nonReentrant {
        if (block.timestamp < mintStart || block.timestamp > mintEnd) {
            revert NotInMintPeriod();
        }

        if (gameInfo.currentRoundId != 0) {
        ...
    }

But in the startGame function, its mint period is mintEnd (exinclude)

    /**
     * @inheritdoc IInfiltration
     * @dev If Chainlink randomness callback does not come back after 1 day, we can call
     *      startNewRound to trigger a new randomness request.
     */
    function startGame() external onlyOwner {
        uint256 numberOfAgents = totalSupply();
        if (numberOfAgents < MAX_SUPPLY) {
            if (block.timestamp < mintEnd) {
                revert StillMinting();
            }
        }

        if (gameInfo.currentRoundId != 0) {
        ...
    }

Impact

This will cause the mint period to end prematurely, preventing users from calling mint

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L502-L502

Tool used

Manual Review

Recommendation

Change < to <=

    /**
     * @inheritdoc IInfiltration
     * @dev If Chainlink randomness callback does not come back after 1 day, we can call
     *      startNewRound to trigger a new randomness request.
     */
    function startGame() external onlyOwner {
        uint256 numberOfAgents = totalSupply();
        if (numberOfAgents < MAX_SUPPLY) {
            if (block.timestamp <= mintEnd) {
                revert StillMinting();
            }
        }

        if (gameInfo.currentRoundId != 0) {
        ...
    }

mstpr-brainbot - New round might be not possible because of high number of loops

mstpr-brainbot

high

New round might be not possible because of high number of loops

Summary

When the NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS is reached the new round will start killing the wounded people either from the round 1 or the round from currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD) depending on the current round state. If the ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD is not reached but the active agents are lesser than 50 then the new round will start killing wounded agents from round 1 to current round. This process can be very gas extensive and cause the function to revert because the gas provided will never be enough. More details on the scenario will be provided in the below section.

Vulnerability Detail

Assume we have 10000 agents (total supply), AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS = 20 and ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD = 48 note that those constant values are directly taken from the actual test file so I am assuming these values are considered to be used.

Now starting from the round 1 to round 48 the wounded agents will not be killed. So assuming the above values let's see how many wounded agents we will have at the round 48 assuming that there are no heals on the wounded agents.

First this is the formula for wounded agents in a given round:

woundedAgentsCount =
            (activeAgents * AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS) /
            ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;

Now, let's plug this formula to Remix to see what would be the wounded agents from round 1 to round 48 assuming there are no heals.

function someMath() external  {
      uint activeAgents = 10_000;
      uint AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS = 20;
      uint ONE_HUNDRED_PERCENT_IN_BASIS_POINTS = 10_000;
      for (uint i; i < 48; ++i) {
        uint res = (activeAgents * AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS) / ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;

        activeAgents -= res;

        r.push(res);
      }
  }

when we execute the above code snippet we will see the array result and the sum of wounded agents as follows:
uint256[]: 20,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18
sum = 892

That means that in 48 rounds there will be 892 wounded agents assuming none of them healed. Since the wounded agents are not counted as "active" agents the total active agents would be 10_000 - 892 = 9108

Now, let's assume at round 48 the active agent count is < 50, that can happen if agents are escapes in that interval + the wounded are not counted. That means we have 892 agents wounded and there are 9060 agents escaped. Which means 10000 - (892 + 9060) = 48 which is lesser than 50 and the next time we call the startNewRound() we will execute the following code:

if (activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {
            uint256 woundedAgents = gameInfo.woundedAgents;

            if (woundedAgents != 0) {
                uint256 killRoundId = currentRoundId > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD
                    ? currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD)
                    : 1;

                // @audit totalSupply() - gameInfo.deadAgents - gameInfo.escapedAgents;
                uint256 agentsRemaining = agentsAlive();
                uint256 totalDeadAgentsFromKilling;
                while (woundedAgentIdsPerRound[killRoundId][0] != 0) {
                    uint256 deadAgentsFromKilling = _killWoundedAgents({
                        roundId: killRoundId,
                        currentRoundAgentsAlive: agentsRemaining // 200
                    });
                    unchecked {
                        totalDeadAgentsFromKilling += deadAgentsFromKilling; // 2
                        agentsRemaining -= deadAgentsFromKilling; // 148
                        ++killRoundId;
                    }
                }

As we can see the while loop will be executed starting from roundId 0 to 48 and starts killing the 892 wounded agents. However, this is extremely high number and the amount of SSTORE and SLOAD is too much. Calling this function will fail because it is not possible to do the loop 48 times for 892+ storage changes. This will result the game to stop and not be able to level up to the next round.

Impact

The game will be impossible to play if the above condition is satisfied which is not an extreme case because escapes can happen randomly and easily. The values for ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD and total supply does not really change this because of the escapes are not limited. Therefore, I'll label this as high.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L579-L651
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1096-L1249
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1404-L1463
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1477-L1508

Tool used

Manual Review

Recommendation

pks_ - Owner can't claim prize when winner burn their NFT

pks_

high

Owner can't claim prize when winner burn their NFT

Summary

Owner can't claim their price in case of they burn their NFT.

Vulnerability Detail

When the winner burn their NFT, the owner can't claim prize because the Infiltration contract depends on the NFT ownership to claim the price by calling Infiltration#claimGrandPrize and Infiltration#claimSecondaryPrizes functions. So when the winner burn their NFT, they can't claim prize anymore.

Impact

The winner can't claim NFT after they burn their NFT.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L659C9-L659C30

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L679C31-L679C31

Tool used

Manual Review

Recommendation

Recommend storing the winner parameter. Add try-catch blocks to each _assertAgentOwnership call. If the call reverts then use storing the winner parameter to transfer price to winner directly.

sil3th - Lack Of Ownership Verification in 'transferFrom' Function

sil3th

high

Lack Of Ownership Verification in 'transferFrom' Function

Summary

The transferFrom function lacks proper ownership verification, allowing unauthorized transfers of agents. Specifically, it does not check if the from address is the owner of the tokenId being transferred.

Vulnerability Detail

The transferFrom function is intended to facilitate the transfer of agents between addresses. However, it lacks a crucial ownership verification step, which means that any address can potentially transfer any agent, regardless of ownership.

Impact

The issue lies in the fact that there is no check to ensure that the from address is the legitimate owner of the agent represented by tokenId. This lack of ownership verification opens up the possibility for unauthorized transfers, potentially leading to another user transferring agents from a tokenId that ain't theirs.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L924-L930

Tool used

Manual Review

Recommendation

To address this bug and ensure proper access control and ownership verification, the transferFrom function should be modified to include a check for owner is equal to the from address as shown below.

function transferFrom(address from, address to, uint256 tokenId) public payable override { address owner = ownerOf(tokenId); // Get the current owner of the agent. require(owner == from, "You are not the owner of this agent.");

mstpr-brainbot - Inconsistency in healing between docs and code, healing is only possible after 2 rounds

mstpr-brainbot

high

Inconsistency in healing between docs and code, healing is only possible after 2 rounds

Summary

The documentation states that healing is possible after 1 round, as evidenced here:
"So, for example, if an agent is wounded in round 12, the first round they can heal is round 13, denoted as EligibleRoundCount = 1."
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/README.md#percentage-chance-to-heal

However, the code implementation permits healing only after 2 rounds. Therefore, the correct statement should be: "The first round they can heal is round 14, not 13."

Vulnerability Detail

As we can see in the following link that the heal is only possible after 2 rounds, assuming an agent is wounded at round 13, earliest possible round that the agent can heal hisself is the round 15 due to this check here:
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L843-L847

Assuming the agent is wounded at round 13 and the current round is 14:
13 - 14 < 2 = 1 < 2 = true = revert.

To give more details, Assuming that the active agents are higher than the secondary wins count for at least 3 more rounds, if the game is in round 13 in order for game to be in round 14 the chainlink VFR needs to call the fullFill function. Once the fulfill function is called the current rounds wounded agents will be determined and the round id will increase to 14. Once the game is in round 14 now we need to wait for a day and call startNewRound this will trigger the VFR and now we can expect an another fulfill call from VFR and the round will be updated to 15. Once the round is 15 now the wounded agent in round 13 can call heal because 15-13 < 2 = 2 < 2 = false so there will be no reverts and function can work.

Impact

This is clearly not consistent with the README of the page so I am assuming this is wrongly implemented hence, I will classify this as high

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L843-L847
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1096-L1196
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L579-L651

Tool used

Manual Review

Recommendation

Either change the doc accordingly or make the if statement as follows:

unchecked {
                if (currentRoundId - woundedAt < 1) {
                    revert HealingMustWaitAtLeastOneRound();
                }
            }

Duplicate of #44

unix515 - Invalid condition - mint will be not worked although native token is sufficient.

unix515

medium

Invalid condition - mint will be not worked although native token is sufficient.

Summary

Invalid condition - mint will be not worked although native token is sufficient.

Vulnerability Detail

If the msg.value is not strictly equal "quantity * PRICE", "premint" and "mint" will be not worked although the native token is sufficiently provided.

Impact

"premint" and "mint" will not be worked although msg.value is sufficiently provided..

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L450
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L482

Tool used

Manual Review

Recommendation

Please correct the condition.

function premint(address to, uint256 quantity) external payable onlyOwner {
--	if (quantity * PRICE != msg.value) {
++	if (quantity * PRICE > msg.value) {
		revert InsufficientNativeTokensSupplied();
	}
	...
}
function mint(uint256 quantity) external payable nonReentrant {
	...
	uint256 amountMinted = amountMintedPerAddress[msg.sender] + quantity;
	if (amountMinted > MAX_MINT_PER_ADDRESS) {
		revert TooManyMinted();
	}

--	if (quantity * PRICE != msg.value) {
++	if (quantity * PRICE > msg.value) {
		revert InsufficientNativeTokensSupplied();
	}

	if (totalSupply() + quantity > MAX_SUPPLY) {
		revert ExceededTotalSupply();
	}

	...
}

mahdiRostami - High Costs Incurred Due to Lack of Business Logic in the Protocol

mahdiRostami

medium

High Costs Incurred Due to Lack of Business Logic in the Protocol

Summary

In the provided docs, a specific game logic is outlined: "Every 50 blocks (approximately 10 minutes), 0.2% of all active agents are randomly set to a 'wounded' status."

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1404-L1416C10

This logic implies that if no one heals or escapes, it takes 2,271 rounds to finish the game. During the last 49 rounds, only one agent will be wounded each round. So in the remaining 2,222 rounds, the outcomes are the following:

  • in 72 rounds 7 agents wounded
  • in 83 rounds 6 agents wounded
  • in 100 rounds 5 agents wounded
  • in 125 rounds 4 agents wounded
  • in 166 rounds 3 agents wounded
  • in 250 rounds 2 agents wounded
  • in 949 rounds 1 agents wounded

As shown above, in 949 rounds, only one agent is wounded. The cost for each round includes LINK and transaction fees,
These 949 rounds constitute 41.79% of the total rounds and will cost 41.79% of all costs.
If we assume each round costs 3 LINK each LINK $10 (I don't consider transaction fees), the total rounds (2271) will cost 68130, and the 949 rounds cost 28470. If in 949 rounds just one agent heals the cost will be double, around 56940.

Vulnerability Detail

https://docs.google.com/spreadsheets/d/1gbaUikPH-q3v-pje9rw__UiHbAt7KNPmoAFj0I9sjS4/edit?usp=sharing

Impact

The protocol incurs significant costs due to this logic.

Code Snippet

        woundedAgentsCount =
            (activeAgents * AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS) /
            ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;
        // At some point the number of agents to wound will be 0 due to round down, so we set it to 1.
        if (woundedAgentsCount == 0) {
            woundedAgentsCount = 1;
        }

Tool used

Manual Review

Recommendation

To reduce costs and improve efficiency, it is suggested to increase 0.2% after some rounds, but if it isn't feasible, it is recommended to modify the code to select 2 wardens instead of 1. By making this change, the number of rounds will decrease from 949 rounds to 474 rounds. This change will lead to substantial cost savings.

        woundedAgentsCount =
            (activeAgents * AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS) /
            ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;
        
+        if (activeAgents < 1000 && activeAgents > 51){ //@audit for agents unders 1000 pick 2 agents
+            woundedAgentsCount = 2;
+        }
        // At some point the number of agents to wound will be 0 due to round down, so we set it to 1.
        if (woundedAgentsCount == 0) {
            woundedAgentsCount = 1;
        }

mstpr-brainbot - Agents can instantly die although the active agents are higher than 50

mstpr-brainbot

high

Agents can instantly die although the active agents are higher than 50

Summary

When alive agents are > 50 the game will keep wounding agents. When the ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD round is surpassed the very first rounds wounded agents will be dead and the next round the second rounds wounded will be dead and keeps going as this. However, when an agent is wounded at the current round but also had wounded in the current round - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD can be instantly dead without having a chance to heal his/herself. When the game has > 50 agents healing is possible but this case will break that and the game will not work as it supposed to.

Vulnerability Detail

Best to go is an example so let's do that!

Assume the variable ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD = 48 note that this constant value is directly taken from the actual test file so I am assuming this value is considered to be used.

Say the game is at the round 49 and the active agent count is 1000 which is more than 50 so the game keeps wounding rather than instant killing phase. Since the rounds to be wounded before dead threshold is over for the round 49, when the next chainlink fulfill call happens there will be some agents wounded at round 49 and also the agents wounded at round (49-48) = 1 will be dead.

Now, say Alice had an agent and she got wounded at round 1 before but she healed herself and now her agent is in ACTIVE mode. When the round 49 processes, Alice agent gets wounded. So, Alice's agent got wounded at round 49 and now since the active agents are more than 50 she still has a chance to heal her agent and keep playing the game. However, as we stated above, since the round is 49 the agents that are wounded at round 1 and still has WOUNDED status will be dead. Remember, Alice also got wounded at round 1 but she healed herself and she got wounded at the current round (49) again. Since the

uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]
            storage woundedAgentIdsInRound = woundedAgentIdsPerRound[1];

still has the Alice's agent index and Alice has just got wounded Alice will be killed at the same round 49 although the kill is supposed to kill the agents that has been wounded in the round 1.

Let's check the kill function and see how Alice gots killed in unfortunate way

function _killWoundedAgents(
        uint256 roundId,
        uint256 currentRoundAgentsAlive
    ) private returns (uint256 deadAgentsCount) {
        uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]
            storage woundedAgentIdsInRound = woundedAgentIdsPerRound[roundId];
        uint256 woundedAgentIdsCount = woundedAgentIdsInRound[0];
        uint256[] memory woundedAgentIds = new uint256[](woundedAgentIdsCount);
        for (uint256 i; i < woundedAgentIdsCount; ) {
          
            uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];

      
            uint256 index = agentIndex(woundedAgentId);
            if (agents[index].status == AgentStatus.Wounded) {
                woundedAgentIds[i] = woundedAgentId;
                _swap({
                    currentAgentIndex: index, // 198
                    lastAgentIndex: currentRoundAgentsAlive - deadAgentsCount, // 199
                    agentId: woundedAgentId, // 200
                    newStatus: AgentStatus.Dead
                });
                unchecked {
                    ++deadAgentsCount;
                }
            }

            unchecked {
                ++i;
            }
        }

        emit Killed(roundId, woundedAgentIds);
    }

As we can see above the function will check the roundId 1 and will spot the Alice index and it will only check whether the Alice has WOUNDED status or not but it will not check which round Alice got wounded. Alice will be immediately killed in the round 49 because she got wounded at round 49 and before she got wounded at round 1 but healed.

Impact

Since this is not an intended behaviour for the game I'll label this as high.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1129-L1143
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1477-L1508

Tool used

Manual Review

Recommendation

Check the "woundedAt" when killing someone or maybe set the woundedAgentIdsInRound index of the healed account to 0.

Duplicate of #21

Krace - Agents with Healing Opportunity Will Be Terminated Directly if The `escape` Reduces activeAgents to the Number of `NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS` or Fewer

Krace

high

Agents with Healing Opportunity Will Be Terminated Directly if The escape Reduces activeAgents to the Number of NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS or Fewer

Summary

Wounded Agents face the risk of losing their last opportunity to heal and are immediately terminated if certain Active Agents decide to escape.

Vulnerability Detail

In each round, agents have the opportunity to either escape or heal before the _requestForRandomness function is called. However, the order of execution between these two functions is not specified, and anyone can be executed at any time just before startNewRound. Typically, this isn't an issue. However, the problem arises when there are only a few Active Agents left in the game.

On one hand, the heal function requires that the number of gameInfo.activeAgents is greater than NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS.

    function heal(uint256[] calldata agentIds) external nonReentrant {
        _assertFrontrunLockIsOff();
//@audit If there are not enough activeAgents, heal is disabled
        if (gameInfo.activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {
            revert HealingDisabled();
        }

On the other hand, the escape function will directly set the status of agents to "ESCAPE" and reduce the count of gameInfo.activeAgents.

   function escape(uint256[] calldata agentIds) external nonReentrant {
        _assertFrontrunLockIsOff();

        uint256 agentIdsCount = agentIds.length;
        _assertNotEmptyAgentIdsArrayProvided(agentIdsCount);

        uint256 activeAgents = gameInfo.activeAgents;
        uint256 activeAgentsAfterEscape = activeAgents - agentIdsCount;
        _assertGameIsNotOverAfterEscape(activeAgentsAfterEscape);

        uint256 currentRoundAgentsAlive = agentsAlive();

        uint256 prizePool = gameInfo.prizePool;
        uint256 secondaryPrizePool = gameInfo.secondaryPrizePool;
        uint256 reward;
        uint256[] memory rewards = new uint256[](agentIdsCount);

        for (uint256 i; i < agentIdsCount; ) {
            uint256 agentId = agentIds[i];
            _assertAgentOwnership(agentId);

            uint256 index = agentIndex(agentId);
            _assertAgentStatus(agents[index], agentId, AgentStatus.Active);

            uint256 totalEscapeValue = prizePool / currentRoundAgentsAlive;
            uint256 rewardForPlayer = (totalEscapeValue * _escapeMultiplier(currentRoundAgentsAlive)) /
                ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;
            rewards[i] = rewardForPlayer;
            reward += rewardForPlayer;

            uint256 rewardToSecondaryPrizePool = (totalEscapeValue.unsafeSubtract(rewardForPlayer) *
                _escapeRewardSplitForSecondaryPrizePool(currentRoundAgentsAlive)) / ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;

            unchecked {
                prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;
            }
            secondaryPrizePool += rewardToSecondaryPrizePool;

            _swap({
                currentAgentIndex: index,
                lastAgentIndex: currentRoundAgentsAlive,
                agentId: agentId,
                newStatus: AgentStatus.Escaped
            });

            unchecked {
                --currentRoundAgentsAlive;
                ++i;
            }
        }

        // This is equivalent to
        // unchecked {
        //     gameInfo.activeAgents = uint16(activeAgentsAfterEscape);
        //     gameInfo.escapedAgents += uint16(agentIdsCount);
        // }

Threrefore, if the heal function is invoked first then the corresponding Wounded Agents will be healed in function fulfillRandomWords. If the escape function is invoked first and the number of gameInfo.activeAgents becomes equal to or less than NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS, the heal function will be disable. This obviously violates the fairness of the game.

Example

Consider the following situation:

After Round N, there are 100 agents alive. And, 1 Active Agent wants to escape and 10 Wounded Agents want to heal.

  • Round N:
    • Active Agents: 51
    • Wounded Agents: 49
    • Healing Agents: 0

According to the order of execution, there are two situations.
Please note that the result is calculated only after _healRequestFulfilled, so therer are no new wounded or dead agents

First, invoking escape before heal.
heal is disable and all Wounded Agents are killed because there are not enough Active Agents.

  • Round N+1:
    • Active Agents: 50
    • Wounded Agents: 0
    • Healing Agents: 0

Second, invoking heal before escape.
Suppose that heal saves 5 agents, and we got:

  • Round N+1:
    • Active Agents: 55
    • Wounded Agents: 39
    • Healing Agents: 0

Obviously, different execution orders lead to drastically different outcomes, which affects the fairness of the game.

Impact

If some Active Agents choose to escape, causing the count of activeAgents to become equal to or less than NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS, the Wounded Agents will lose their final chance to heal themselves.

This situation can significantly impact the game's fairness. The Wounded Agents would have otherwise had the opportunity to heal themselves and continue participating in the game. However, the escape of other agents leads to their immediate termination, depriving them of that chance.

Code Snippet

Heal will be disabled if there are not enout activeAgents.
Infiltration.sol#L804

Escape will directly reduce the activeAgents.
Infiltration.sol#L769

Tool used

Manual Review

Recommendation

It is advisable to ensure that the escape function is always called after the heal function in every round. This guarantees that every wounded agent has the opportunity to heal themselves when there are a sufficient number of activeAgents at the start of each round. This approach can enhance fairness and gameplay balance.

Beosin - Algorithm error

Beosin

medium

Algorithm error

##Summary
Inconsistency in calculations related to the HEAL_PROBABILITY_MINUEND parameter

##Vulnerability Detail
Inconsistency between the parameters used in the initialization HEAL_PROBABILITY_MINUEND and subsequent calculations in the healProbability function.
The algorithm for calculating HEAL_PROBABILITY_MINUEND is *99-80. the calculation in HEALProbability is *19 (which is 99-80) .

##Impact
The result of the healProbability function calculation is large, affecting the token destruction in _healRequestFulfilled, and the return value deadAgentsCount is not increased. The end of the game was delayed.

##Code Snippet
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L379-L381

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1019-L1028
##Tool used
Manual Review

##Recommendation
It is recommended to modify the HEAL_PROBABILITY_MINUEND parameter or the calculation in the healProbability function.

mstpr-brainbot - Chainlink VRF variables are hardcoded

mstpr-brainbot

high

Chainlink VRF variables are hardcoded

Summary

When interacting with the Chainlink VRF, the code utilizes constant variables for minimumRequestConfirmations and callbackGasLimit. These variables are verified within the Chainlink VRF contract. However, the validation criteria for these variables can be modified by the VRF contract administrator. If these criteria are altered such that they are incompatible with the current hardcoded values, the game will not function as intended.

Vulnerability Detail

When the chainlink requestRandomWords function triggered the values for callbackGasLimit and minimumRequestConfirmations are hardcoded. Those values are validated inside the chainlink function and can be changed to different values via the chainlink admin. If chainlink decides to change those values in an unfavorable value that would make the game to be unable to call chainlink due to validation reverts then the game will be stuck.

Example:
Assume that the chainlinks s_config.minimumRequestConfirmations storage variable is set to 5 by chainlink owner. Since the game contract calls the chainlink VFR as follows:

uint256 requestId = VRF_COORDINATOR.requestRandomWords({
            keyHash: KEY_HASH,
            subId: SUBSCRIPTION_ID,
            minimumRequestConfirmations: uint16(3),
            callbackGasLimit: uint32(2_500_000),
            numWords: uint32(1)
        });

the transaction will revert because of the minimumRequestConfirmations(3) is lesser than the s_config.minimumRequestConfirmations(5)
https://github.com/smartcontractkit/chainlink/blob/5cc88a82ec77b1d4f3f37b0255af778a056e7e5c/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol#L367C13-L375

Another scenario focusing on the callback gas limit:
If we take this scenario #4 there can be 800+ wounded agents and the probability of this loop looping more than "x" times such that the callback gas limit is not enough is pretty possible

for (uint256 i; i < woundedAgentsCount; ) {
            uint256 woundedAgentIndex = (randomWord % currentRoundAgentsAlive).unsafeAdd(1);
            Agent storage agentToWound = agents[woundedAgentIndex];

            if (agentToWound.status == AgentStatus.Active) {
                // This is equivalent to
                // agentToWound.status = AgentStatus.Wounded;
                // agentToWound.woundedAt = roundId;
                assembly {
                    let agentSlotValue := sload(agentToWound.slot)
                    agentSlotValue := and(
                        agentSlotValue,
                        // This is equivalent to
                        // or(
                        //     TWO_BYTES_BITMASK,
                        //     shl(64, TWO_BYTES_BITMASK)
                        // )
                        0x00000000000000000000000000000000000000000000ffff000000000000ffff
                    )
                    // AgentStatus.Wounded is 1
                    agentSlotValue := or(agentSlotValue, shl(AGENT__STATUS_OFFSET, 1))
                    agentSlotValue := or(agentSlotValue, shl(AGENT__WOUNDED_AT_OFFSET, roundId))
                    sstore(agentToWound.slot, agentSlotValue)
                }

                uint256 woundedAgentId = _agentIndexToId(agentToWound, woundedAgentIndex);
                woundedAgentIds[i] = woundedAgentId;

                unchecked {
                    ++i;
                    currentRoundWoundedAgentIds[i] = uint16(woundedAgentId);
                }

                // @audit randomWord = uint256(keccak256(abi.encode(randomWord)));
                randomWord = _nextRandomWord(randomWord);
            } else {
                // If no agent is wounded using the current random word, increment by 1 and retry.
                // If overflow, it will wrap around to 0.
                unchecked {
                    ++randomWord;
                }
            }
        }

assuming 10000 agents alive and 892 wounded the probability of hitting an agent that is wounded is 892/10000 although that in such scenario the randomWord is incremented by ++ would possibly make things easier (it tries to look the closest sample) it is still has a chance to loop more than the required callback gas limit.

Impact

I will label this as high for the following reasons:

1- Sherlock page for the contest states:

In case of external protocol integrations, are the risks of external contracts pausing or executing an emergency withdrawal acceptable? If not, Watsons will submit issues related to these situations that can harm your protocol's functionality.

Yes they are acceptable

2- This issue will make the game to stop and there are no fixes then emergency withdraw

3- Looks team does not have control on chainlink

4- Chainlink might see an issue with the current value and they mandatory set the new value higher than 3 for security purposes meaning that they can not lower it down for Looks team even if they wanted to

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1294-L1300
Chainlink
https://github.com/smartcontractkit/chainlink/blob/5cc88a82ec77b1d4f3f37b0255af778a056e7e5c/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol#L218-L253
https://github.com/smartcontractkit/chainlink/blob/5cc88a82ec77b1d4f3f37b0255af778a056e7e5c/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol#L368-L382

Tool used

Manual Review

Recommendation

Make setters functions for the values that can be changed by chainlink admin

unix515 - Unchecked return value for IERC20.approve call

unix515

medium

Unchecked return value for IERC20.approve call

Summary

The return value for ERC20.approve call is not checked.

Vulnerability Detail

"heal" function of "InfiltrationPeriphery" contract calls IERC20.approve but does not check the success return value.
Some tokens do not revert if the approval failed but return false instead.

Impact

Although approve is failed, transfer logic will be not stopped/

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery#L60

Tool used

Manual Review

Recommendation

function heal(uint256[] calldata agentIds) external payable {
	uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);
	uint256 amountIn = SWAP_ROUTER.exactOutputSingle{value: msg.value}(params);
	
--	IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);
++	if(!IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS))
++      // add revert logic
	
	INFILTRATION.heal(agentIds);
	...	
}

cergyk - Incorrect bitmask used in _swap will lead to ids in agents mapping to be corrupted

cergyk

high

Incorrect bitmask used in _swap will lead to ids in agents mapping to be corrupted

Summary

Infiltration smart contract uses a common pattern when handling deletion in arrays:
Say we want to delete element i in array of length N:

1/ We swap element at index i with element at index N-1
2/ We update the length of the array to be N-1

Infiltration uses _swap() method to do this with a twist:
It updates the status of the removed agent to Killed, and initializes id of last agent if it is uninitialized.

There is a mistake in a bitmask value used for resetting the id of the last agent, which will mess up the game state.

Lines of interest for reference:
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1582-L1592

Vulnerability Detail

Let's take a look at the custom assembly block used to assign an id to the newly allocated lastAgent:

assembly {
    let lastAgentCurrentValue := sload(lastAgentSlot)
    // Replace the last agent's ID with the current agent's ID.
--> lastAgentCurrentValue := and(lastAgentCurrentValue, not(AGENT__STATUS_OFFSET))
    lastAgentCurrentValue := or(lastAgentCurrentValue, lastAgentId)
    sstore(currentAgentSlot, lastAgentCurrentValue)

    let lastAgentNewValue := agentId
    lastAgentNewValue := or(lastAgentNewValue, shl(AGENT__STATUS_OFFSET, newStatus))
    sstore(lastAgentSlot, lastAgentNewValue)
}

The emphasized line shows that not(AGENT__STATUS_OFFSET) is used as a bitmask to set the agentId value to zero.
However AGENT__STATUS_OFFSET == 16, and this means that instead of setting the low-end 2 bytes to zero, this will the single low-end fifth bit to zero. Since bitwise or is later used to assign lastAgentId, if the id in lastAgentCurrentValue is not zero, and is different from lastAgentId, the resulting value is arbitrary, and can cause the agent to be unrecoverable.

The desired value for the mask is not(TWO_BYTES_BITMASK):

    not(AGENT__STATUS_OFFSET) == 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef
    not(TWO_BYTES_BITMASK)    == 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000

Impact

The ids of the agents will be mixed, this could mean that some agents will be unrecoverable, or duplicated.
Since the attribution of the grand prize relies on the agentId stored, we may consider that the legitimate winner can lose access to the prize.

Code Snippet

Tool used

Manual Review

Recommendation

assembly {
    let lastAgentCurrentValue := sload(lastAgentSlot)
    // Replace the last agent's ID with the current agent's ID.
-   lastAgentCurrentValue := and(lastAgentCurrentValue, not(AGENT__STATUS_OFFSET))
+   lastAgentCurrentValue := and(lastAgentCurrentValue, not(TWO_BYTES_BITMASK))
    lastAgentCurrentValue := or(lastAgentCurrentValue, lastAgentId)
    sstore(currentAgentSlot, lastAgentCurrentValue)

    let lastAgentNewValue := agentId
    lastAgentNewValue := or(lastAgentNewValue, shl(AGENT__STATUS_OFFSET, newStatus))
    sstore(lastAgentSlot, lastAgentNewValue)
}

0xruffbuff.eth - Potential Vulnerabilities

0xruffbuff.eth

high

Potential Vulnerabilities

Summary

The smart contract code has several areas where improvements are required to ensure data integrity and prevent potential attacks.

Vulnerability Detail

Unchecked Array Indexing:
The function _healRequestFulfilled does not ensure that healingAgentIdsCount is within the bounds of the healingAgentIds array, which can lead to out-of-bounds access.

Impact

Out-of-bounds access could lead to incorrect data being read or written, which could corrupt the contract state or lead to unexpected behavior.

Code Snippet

Code location
// @audit-issue: Unchecked array indexing can lead to out-of-bounds access.
uint256 healingAgentId = healingAgentIds[i.unsafeAdd(1)];

Tool used

Manual Review

Recommendation

Implement bounds checking to ensure that the index is within the valid range before accessing the array.
Consider using a library or utility function to handle array access in a safe manner.

Duplicate of #144

cergyk - Weak randomness in _woundRequestFulfilled can be slightly manipulated

cergyk

medium

Weak randomness in _woundRequestFulfilled can be slightly manipulated

Summary

When the Chainlink VRF provides a random word, the function _woundRequestFulfilled is called.
This function chooses an agent which is alive at random, and if the agent is Active, it marks it as Wounded.
However, if the agent is already Wounded or Healing, the randomWord is modified and another agent is chosen.

The problem lies in the fact that instead of being changed entirely, randomWord is only incremented here. In fact randomWord is incremented as many times as needed to find an active agent.
This is equivalent to checking sequentially after the first actually randomly chosen agent. This means that active agents which are right after a sequence of healed/wounded agents have more chances to be wounded in a round.

Vulnerability Detail

Let's check an extreme example:

Prerequisites

  • Currently alive agents: 100
  • Alice has 40 agents, ranked from 61 to 100, in the Wounded state
  • Bob holds the 1st agent

Analysis

All agents from rankings 2 to 60 have 1% chance to be wounded
Agent ranked 1 has 40%, because if any Alice's agent is chosen to be wounded, the sequential enumeration wrap around back to agent ranked 1.

Impact

Some players with a large number of agents can manipulate the randomness to their advantage and make other users pay more to heal their agents (they are wounded more often).

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1457-L1462

Tool used

Manual Review

Recommendation

use the function _nextRandomWord() here similarly to _healRequestFulfilled to choose the next agent to be hit

Duplicate of #84

klaus - _killWoundedAgents - re-wounded healed agent die when ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD passed because of not checking woundedAt

klaus

high

_killWoundedAgents - re-wounded healed agent die when ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD passed because of not checking woundedAt

Summary

If an agent healed is wounded again, the agent will die from the previous wound that was healed. The user spends LOOKS tokens to heal and success to heal, but as the result, the agent will die.

Vulnerability Detail

The _killWoundedAgents function only checks the status of the agent, not when it was wounded.

    function _killWoundedAgents(
        uint256 roundId,
        uint256 currentRoundAgentsAlive
    ) private returns (uint256 deadAgentsCount) {
        ...
        for (uint256 i; i < woundedAgentIdsCount; ) {
            uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];

            uint256 index = agentIndex(woundedAgentId);
@>          if (agents[index].status == AgentStatus.Wounded) {
                ...
            }

            ...
        }

        emit Killed(roundId, woundedAgentIds);
    }

So when fulfillRandomWords kills agents that were wounded and unhealed at round currentRoundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD, it will also kill the agent who was healed and wounded again after that round.

Also, since fulfillRandomWords first draws the new wounded agents before kills agents, in the worst case scenario, agent could die immediately after being wounded in this round.

if (activeAgents > NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {
@>  uint256 woundedAgents = _woundRequestFulfilled(
        currentRoundId,
        currentRoundAgentsAlive,
        activeAgents,
        currentRandomWord
    );

    uint256 deadAgentsFromKilling;
    if (currentRoundId > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD) {
@>      deadAgentsFromKilling = _killWoundedAgents({
            roundId: currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD),
            currentRoundAgentsAlive: currentRoundAgentsAlive
        });
    }

This is the PoC test code. You can add it to the Infiltration.fulfillRandomWords.t.sol file and run it.

function test_poc() public {

    _startGameAndDrawOneRound();

    uint256[] memory randomWords = _randomWords();
    uint256[] memory woundedAgentIds;

    for (uint256 roundId = 2; roundId <= ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD + 1; roundId++) {

        if(roundId == 2) { // heal agent. only woundedAgentIds[0] dead.
            (woundedAgentIds, ) = infiltration.getRoundInfo({roundId: 1});
            assertEq(woundedAgentIds.length, 20);

            _drawXRounds(1);

            _heal({roundId: 3, woundedAgentIds: woundedAgentIds});

            _startNewRound();

            // everyone except woundedAgentIds[0] is healed
            uint256 agentIdThatWasKilled = woundedAgentIds[0];

            IInfiltration.HealResult[] memory healResults = new IInfiltration.HealResult[](20);
            for (uint256 i; i < 20; i++) {
                healResults[i].agentId = woundedAgentIds[i];

                if (woundedAgentIds[i] == agentIdThatWasKilled) {
                    healResults[i].outcome = IInfiltration.HealOutcome.Killed;
                } else {
                    healResults[i].outcome = IInfiltration.HealOutcome.Healed;
                }
            }

            expectEmitCheckAll();
            emit HealRequestFulfilled(3, healResults);

            expectEmitCheckAll();
            emit RoundStarted(4);

            randomWords[0] = (69 * 10_000_000_000) + 9_900_000_000; // survival rate 99%, first one gets killed

            vm.prank(VRF_COORDINATOR);
            VRFConsumerBaseV2(address(infiltration)).rawFulfillRandomWords(_computeVrfRequestId(3), randomWords);

            for (uint256 i; i < woundedAgentIds.length; i++) {
                if (woundedAgentIds[i] != agentIdThatWasKilled) {
                    _assertHealedAgent(woundedAgentIds[i]);
                }
            }

            roundId += 2; // round 2, 3 used for healing
        }

        _startNewRound();

        // Just so that each round has different random words
        randomWords[0] += roundId;

        if (roundId == ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD + 1) { // wounded agents at round 1 are healed, only woundedAgentIds[0] was dead.
            (uint256[] memory woundedAgentIdsFromRound, ) = infiltration.getRoundInfo({
                roundId: uint40(roundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD)
            });

            // find re-wounded agent after healed
            uint256[] memory woundedAfterHeal = new uint256[](woundedAgentIds.length);
            uint256 totalWoundedAfterHeal;
            for (uint256 i; i < woundedAgentIds.length; i ++){
                uint256 index = infiltration.agentIndex(woundedAgentIds[i]);
                IInfiltration.Agent memory agent = infiltration.getAgent(index);
                if (agent.status == IInfiltration.AgentStatus.Wounded) {
                    woundedAfterHeal[i] = woundedAgentIds[i]; // re-wounded agent will be killed
                    totalWoundedAfterHeal++;
                }
                else{
                    woundedAfterHeal[i] = 0; // set not wounded again 0
                }

            }
            expectEmitCheckAll();
            emit Killed(roundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD, woundedAfterHeal);
        }

        expectEmitCheckAll();
        emit RoundStarted(roundId + 1);

        uint256 requestId = _computeVrfRequestId(uint64(roundId));
        vm.prank(VRF_COORDINATOR);
        VRFConsumerBaseV2(address(infiltration)).rawFulfillRandomWords(requestId, randomWords);
    }
}

Impact

The user pays tokens to keep the agent alive, but agent will die even if agent success to healed. The user has lost tokens and is forced out of the game.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1489

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1130-L1143

Tool used

Manual Review

Recommendation

Check woundedAt at _killWoundedAgents

    function _killWoundedAgents(
        uint256 roundId,
        uint256 currentRoundAgentsAlive
    ) private returns (uint256 deadAgentsCount) {
        ...
        for (uint256 i; i < woundedAgentIdsCount; ) {
            uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];

            uint256 index = agentIndex(woundedAgentId);
-           if (agents[index].status == AgentStatus.Wounded) {
+           if (agents[index].status == AgentStatus.Wounded && agents[index].woundedAt == roundId) {
                ...
            }

            ...
        }

        emit Killed(roundId, woundedAgentIds);
    }

Nadin - `POOL_FEE` is hardcoded which will lead to significant losses compared to optimal routing

Nadin

medium

POOL_FEE is hardcoded which will lead to significant losses compared to optimal routing

Summary

In InfiltrationPeriphery.sol, POOL_FEE is hardcoded, which reduce significantly the possibilities and will lead to non optimal routes.

Vulnerability Detail

  • POOL_FEE is hardcoded = 3_000 (0.3%) and is used in the heal() function and the costToHeal() function.
  • The main issue is the assumption and usage of the 0,3% fee pool regardless of asset. Those that are familiar with UniV3 will know that there are multiple pool tiers for the same asset pair. Hence, it is possible that there are other pools (Eg. the pool with 0.1% fee) where majority of the liquidity lies instead.
  • Therefore using the current implementation would create a significant loss of revenue.

Impact

POOL_FEE is hardcoded, which reduce significantly the possibilities and will lead to non optimal routes.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L19

Tool used

Manual Review

Recommendation

Enable skipping conversion for some assets, and have a mapping of the pool fee as part of the customisable configuration so that pools with the best liquidity can be used.

cergyk - A participant with enough agents can force win while some opponents' agents are healing

cergyk

high

A participant with enough agents can force win while some opponents' agents are healing

Summary

Infiltration contract decides the game is over when only one Active agent is left.
This fails to take in account that some agents may be Healing and may become active again in next round.
This makes sense if number of active agents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS in previous round already, since at that point no healing is allowed.
But one participant holding 100% of active agents and at least N=NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS+1 agents, can escape N agents, and claim the grand prize right away.

Vulnerability Detail

Let's examine the following scenario:

NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS = 50

Alice holds 51 active agents, Bob holds 10 healing agents.

Bob cannot win anymore, even though his agents could've become active again if another round was started.

Impact

Participants may be unfairly denied the healing settlement, thus losing the right to potentially win grand prize. That also means these participants paid healing fee for nothing in this round.

Code Snippet

Tool used

Manual Review

Recommendation

Use (gameInfo.activeAgents == 1 && gameInfo.healingAgents == 0) as a criteria for the game being over;

function _assertGameOver() private view {
-   if (gameInfo.activeAgents != 1) {
+   if (gameInfo.activeAgents != 1 || gameInfo.healingAgents != 0) {
        revert GameIsStillRunning();
    }
}

and in startNewRound:

-if (activeAgents == 1) {
+if (gameInfo.activeAgents == 1 && gameInfo.healingAgents == 0)
    revert GameOver();
}

Duplicate of #98

Beosin - Algorithm error

Beosin

medium

Algorithm error

Summary

Inconsistency in calculations related to the HEAL_PROBABILITY_MINUEND parameter

Vulnerability Detail

Inconsistency between the parameters used in the initialization HEAL_PROBABILITY_MINUEND and subsequent calculations in the healProbability function.
The algorithm for calculating HEAL_PROBABILITY_MINUEND is *99-80. the calculation in HEALProbability is *19 (which is 99-80) .

Impact

The result of the healProbability function calculation is large, affecting the token destruction in _healRequestFulfilled, and the return value deadAgentsCount is not increased. The end of the game was delayed.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L379-L381
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1019-L1028

Tool used

Manual Review

Recommendation

It is recommended to modify the HEAL_PROBABILITY_MINUEND parameter or the calculation in the healProbability function.

Duplicate of #14

0xweb3boy - `Infiltration.sol :: escape()` Agents escaping the game will get lesser rewards than expected due to precision loss

0xweb3boy

high

Infiltration.sol :: escape() Agents escaping the game will get lesser rewards than expected due to precision loss

Summary

escape() function is used for agents to escape the game and take a part of the rewards from the pool, which is wrongly calculated and hence the agents get lesser rewards than expected.

Vulnerability Detail

escape() function is used for agents to escape the game and take a part of the rewards from the pool, and the more participants in the game the smaller the rewards will be since the escape multiplier is smaller.
But the rewards depends upon the _escapeMultiplier and which has rounding errors due to division before multiplication.

The function escape() is calling internally _escapeMultiplier() to calculate rewardForPlayer as shown in the code snippet below :

 uint256 rewardForPlayer = (totalEscapeValue * _escapeMultiplier(currentRoundAgentsAlive)) / ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;

Now if you closely see the function the multiplier is calculated with the equation which has division before multiplication which will round down the values to the nearest integer and which further will result in the agents getting lesser rewards than the expected amount.

 function _escapeMultiplier(uint256 agentsRemaining) private view returns (uint256 multiplier) {
       multiplier =
           ((80 *
               ONE_HUNDRED_PERCENT_IN_BASIS_POINTS_SQUARED -
               50 *
               (((agentsRemaining * ONE_HUNDRED_PERCENT_IN_BASIS_POINTS) / totalSupply()) ** 2)) * 100) / 
           ONE_HUNDRED_PERCENT_IN_BASIS_POINTS_SQUARED;
   }

Impact

Agents getting less rewards than the expected . Since there is fund loss incurred for the agents I am rating this as a high vulnerability.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L741
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1626-L1633

Tool used

Manual Review

Recommendation

Recommendation for the protocol is to avoid divison before multiplication and always perform division operation at last.

Duplicate of #17

coffiasd - Wound agent can't invoke heal in the next round

coffiasd

medium

Wound agent can't invoke heal in the next round

Summary

According to the document:

if a user dies on round 12. The first round they can heal is round 13
However incorrect current round id check led to users being unable to invoke the heal function in the next round.

Vulnerability Detail

Assume players being marked as wounded in the round 12 , players cannot invoke heal in the next round 13

    function test_heal_in_next_round_v1() public {
        _startGameAndDrawOneRound();

        _drawXRounds(11);


        (uint256[] memory woundedAgentIds, ) = infiltration.getRoundInfo({roundId: 12});

        address agentOwner = _ownerOf(woundedAgentIds[0]);
        looks.mint(agentOwner, HEAL_BASE_COST);

        vm.startPrank(agentOwner);
        _grantLooksApprovals();
        looks.approve(TRANSFER_MANAGER, HEAL_BASE_COST);

        uint256[] memory agentIds = new uint256[](1);
        agentIds[0] = woundedAgentIds[0];

        uint256[] memory costs = new uint256[](1);
        costs[0] = HEAL_BASE_COST;

        //get gameInfo
        (,,,,,uint40 currentRoundId,,,,,) = infiltration.gameInfo();
        assert(currentRoundId == 13);

        //get agent Info
        IInfiltration.Agent memory agentInfo = infiltration.getAgent(woundedAgentIds[0]);
        assert(agentInfo.woundedAt == 12);

        //agent can't invoke heal in the next round.
        vm.expectRevert(IInfiltration.HealingMustWaitAtLeastOneRound.selector);
        infiltration.heal(agentIds);
    }

Impact

User have to wait for 1 more round which led to the odds for an Agent to heal successfully start at 99% at Round 1 reduce to 98% at Round 2.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L843#L847

    // No need to check if the heal deadline has passed as the agent would be killed
    unchecked {
        if (currentRoundId - woundedAt < 2) {
            revert HealingMustWaitAtLeastOneRound();
        }
    }

Tool used

Manual Review

Recommendation

    // No need to check if the heal deadline has passed as the agent would be killed
    unchecked {
-       if (currentRoundId - woundedAt < 2) {
-       if (currentRoundId - woundedAt < 1) {
            revert HealingMustWaitAtLeastOneRound();
        }
    }

Beosin - Algorithm error

Beosin

false

Algorithm error

Summary
Inconsistency in calculations related to the HEAL_PROBABILITY_MINUEND parameter

Vulnerability Detail
Inconsistency between the parameters used in the initialization HEAL_PROBABILITY_MINUEND and subsequent calculations in the healProbability function.
The algorithm for calculating HEAL_PROBABILITY_MINUEND is *99-80. the calculation in HEALProbability is *19 (which is 99-80) .

Impact
The result of the healProbability function calculation is large, affecting the token destruction in _healRequestFulfilled, and the return value deadAgentsCount is not increased. The end of the game was delayed.

Code Snippet
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L379-L381

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1019-L1028
Tool used
Manual Review

Recommendation
It is recommended to modify the HEAL_PROBABILITY_MINUEND parameter or the calculation in the healProbability function.

gin - Potential DOS if user provide more ether than required

gin

medium

Potential DOS if user provide more ether than required

Summary

Vulnerability Detail

Using exact comparison for value provided and required, there is a chance user provide more than needed but getting rejected for minting

   if (quantity * PRICE != msg.value) {         
            revert InsufficientNativeTokensSupplied();
        }

Impact

Unnecessary denial of service for minting transaction

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L450
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L482

Tool used

Manual Review

RecommendationU

se > instead of !=

 if (quantity * PRICE > msg.value) {           
            revert InsufficientNativeTokensSupplied();
        }

Duplicate of #1

tpiliposian - Malicious escape may lead to protocol funds loss or insolvency

tpiliposian

high

Malicious escape may lead to protocol funds loss or insolvency

Summary

In the Infiltration.sol contract there is escape() function, which calculates prizePool in a unchecked block, which can lead to protocol funds loss or even insolvency in certain cases.

            unchecked {
                prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;
            }

Vulnerability Detail

In case when there are two agents left, calling escape() function can lead to protocol funds loss or even insolvency, as prizePool can underflow in a scenario such as this:

There are 2 active agents left from 50 total agents.
The prize pool is 1 ETH.
The user decides to escape from the game using the escape() function using their agentIds.
The rewardForPlayer is being calculated in this way: (totalEscapeValue *_escapeMultiplier(currentRoundAgentsAlive)) /ONE_HUNDRED_PERCENT_IN_BASIS_POINTS; where function _escapeMultiplier() calculated this way:

    function _escapeMultiplier(uint256 agentsRemaining) private view returns (uint256 multiplier) {
        multiplier =
            ((80 *
                ONE_HUNDRED_PERCENT_IN_BASIS_POINTS_SQUARED -
                50 *
                (((agentsRemaining * ONE_HUNDRED_PERCENT_IN_BASIS_POINTS) / totalSupply()) ** 2)) * 100) /
            ONE_HUNDRED_PERCENT_IN_BASIS_POINTS_SQUARED;
    }

Plugging in the numbers we get this equation which gets us the following number:
((80*10**18 - 50 * (((2*10**8)/50)**2 * 100))/10**8 * (10**18 / 2))/10**8 which is equal to 3.996e+21

Then this is used in prizePool calculation:

                prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;

Impact

Doing the calculations above already the RewardForPlayer is bigger then the prizePool which leads to underflow and subsequently to an absurdly big number for the prizePool which the contract either can't redeem which will lead to insolvency or
in the case the contract has enough funds that will lead to the theft of all of the funds from the contract and/or the winner not receiving his prize for the first place..

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L716-L796

Tool used

Manual Review

Recommendation

Remove unchecked block from prizePool calculations from the escape() function:

--            unchecked {
--                prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;
--            }

++                prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;

dany.armstrong90 - The user can mint more than MAX_MINT_PER_ADDRESS.

dany.armstrong90

medium

The user can mint more than MAX_MINT_PER_ADDRESS.

Summary

The user can mint more than MAX_MINT_PER_ADDRESS because of the owner's Infiltration.sol#premint call.

Vulnerability Detail

The Infiltration.sol#premint function is as follows.

File: Infiltration.sol
449:     function premint(address to, uint256 quantity) external payable onlyOwner {
450:         if (quantity * PRICE != msg.value) {
451:             revert InsufficientNativeTokensSupplied();
452:         }
453: 
454:         if (totalSupply() + quantity > MAX_SUPPLY) {
455:             revert ExceededTotalSupply();
456:         }
457: 
458:         if (gameInfo.currentRoundId != 0) {
459:             revert GameAlreadyBegun();
460:         }
461: 
462:         _mintERC2309(to, quantity);
463:     }

As we can see above, amountMintedPerAddress data is not updated about to address and quantity amount.
On the other hand, in Infiltration.sol#mint the system restricts amount which a user can mint.

File: Infiltration.sol
468:     function mint(uint256 quantity) external payable nonReentrant {
469:         if (block.timestamp < mintStart || block.timestamp > mintEnd) {
470:             revert NotInMintPeriod();
471:         }
472: 
473:         if (gameInfo.currentRoundId != 0) {
474:             revert GameAlreadyBegun();
475:         }
476: 
477:         uint256 amountMinted = amountMintedPerAddress[msg.sender] + quantity;
478:         if (amountMinted > MAX_MINT_PER_ADDRESS) {
479:             revert TooManyMinted();
480:         }
481: 
482:         if (quantity * PRICE != msg.value) {
483:             revert InsufficientNativeTokensSupplied();
484:         }
485: 
486:         if (totalSupply() + quantity > MAX_SUPPLY) {
487:             revert ExceededTotalSupply();
488:         }
489: 
490:         amountMintedPerAddress[msg.sender] = amountMinted;
491:         _mintERC2309(msg.sender, quantity);
492:     }

So a user can obtain more agents than MAX_MINT_PER_ADDRESS if owner preminted some amount for him.
Then, the user can get much more probability than other users to gain rewards.

This can result in discouraging participants.

Impact

The user can mint more than MAX_MINT_PER_ADDRESS. This can result in discouraging participants.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L449

Tool used

Manual Review

Recommendation

The Infiltration.sol#premint function has to modified as follows.

File: Infiltration.sol
449:     function premint(address to, uint256 quantity) external payable onlyOwner {
450:         if (quantity * PRICE != msg.value) {
451:             revert InsufficientNativeTokensSupplied();
452:         }

++           uint256 amountMinted = amountMintedPerAddress[to] + quantity;
++           if (amountMinted > MAX_MINT_PER_ADDRESS) {
++               revert TooManyMinted();
++           }

454:         if (totalSupply() + quantity > MAX_SUPPLY) {
455:             revert ExceededTotalSupply();
456:         }
457: 
458:         if (gameInfo.currentRoundId != 0) {
459:             revert GameAlreadyBegun();
460:         }

++           amountMintedPerAddress[to] = amountMinted;

462:         _mintERC2309(to, quantity);
463:     }

cergyk - An agent wounded-healed-wounded during ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD rounds can be unjustly killed

cergyk

high

An agent wounded-healed-wounded during ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD rounds can be unjustly killed

Summary

When an agent is wounded, they have ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD rounds to initiate healing, otherwise the contract will mark them as Dead during new round.

However the contract fails to take in account that an agent can heal and be wounded again before ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD has elapsed, and in that case would kill the agent, as if they were never healed.

Vulnerability Detail

We see that the smart contract keeps track of agents which are wounded at some round R, with the variable:
woundedAgentIdsPerRound[R], which is an array of ids.

This way when initializing a new round in fulfillRandomness, the call iterates over the agent ids located at index:

roundWoundedToKill = currentRound - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD

And marks as Dead any agent which is still in the state Wounded.
However if the agent has healed, and been wounded again between roundWoundedToKill and currentRound, he would be unjustly marked as Dead, when he has only been wounded recently.

See the implementation of _killWoundedAgents:
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1477-L1508

Impact

Some agents may be unjustly marked as dead during the game, participants unjustly lose their shot at winning a prize (loss of funds for the participants).

Code Snippet

Tool used

Manual Review

Recommendation

Modify _killWoundedAgents to check the woundedAt variable:

function _killWoundedAgents(
    uint256 roundId,
    uint256 currentRoundAgentsAlive
) private returns (uint256 deadAgentsCount) {
    uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]
        storage woundedAgentIdsInRound = woundedAgentIdsPerRound[roundId];
    uint256 woundedAgentIdsCount = woundedAgentIdsInRound[0];
    uint256[] memory woundedAgentIds = new uint256[](woundedAgentIdsCount);
    for (uint256 i; i < woundedAgentIdsCount; ) {
        uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];

        uint256 index = agentIndex(woundedAgentId);
        
        if (agents[index].status == AgentStatus.Wounded && 
+            //Ensure agents were last wounded at that round, and not healed/rewounded
+            agents[index].woundedAt == roundId
        ) {
            woundedAgentIds[i] = woundedAgentId;
            _swap({
                currentAgentIndex: index,
                lastAgentIndex: currentRoundAgentsAlive - deadAgentsCount,
                agentId: woundedAgentId,
                newStatus: AgentStatus.Dead
            });
            unchecked {
                ++deadAgentsCount;
            }
        }

        unchecked {
            ++i;
        }
    }

    emit Killed(roundId, woundedAgentIds);
}

Duplicate of #21

tpiliposian - Missing mint caps

tpiliposian

medium

Missing mint caps

Summary

The Infiltration.sol contract inherits from ERC721A.
ERC721A has a quantity limit of 5000:

uint256 private constant _MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000;

https://github.com/chiru-labs/ERC721A/blob/main/contracts/ERC721A.sol

on _mintERC2309() function.
Currently, there is no check for quantity parameter in premint() and mint() functions of our contract.

Vulnerability Detail

Our contract inherits from ERC721A which packs balance, numberMinted, numberBurned, and
an extra data chunk in 1 storage slot (64 bits per substorage) for every address. This would add an inherent
cap of 2^64-1 to all these different fields. Currently, there is no check for quantity.

Also, if we almost reach the max cap for a balance by an owner and someone else transfers a token to this owner,
there would be an overflow for the balance and possibly the number of mints in the _packedAddressData:
https://github.com/chiru-labs/ERC721A/blob/dca00fffdc8978ef517fa2bb6a5a776b544c002a/contracts/ERC721A.sol#L121-L128

Impact

The overflow could reduce the balance and the numberMinted to a way lower number and numberBurned to a
way higher number.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L449-L463

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L468-L492

Tool used

Manual Review

Recommendation

Add an additional check if quantity would exceed the mint cap before calling _mintERC2309.

hassan-truscova - Infiltration._killWoundedAgents eventually will revert because of block gas limit

hassan-truscova

medium

Infiltration._killWoundedAgents eventually will revert because of block gas limit

Summary

Infiltration._killWoundedAgents eventually will revert because of block gas limit

Vulnerability Detail

Infiltration._killWoundedAgents function is looping through all wounded agents and does at least woundedAgentIdsCount amount of external calls/swaps (when all agents are not wounded). This all consumes a lot of gas and each new wounded agent increases loop size. It means that after some time woundedAgentIdsPerRound mapping will be big enough that the gas amount sent for function will be not enough to retrieve all data. So the function will revert.

Impact

Function will always revert and whoever depends on it will not be able to get information.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1477-L1505

    function _killWoundedAgents(
        uint256 roundId,
        uint256 currentRoundAgentsAlive
    ) private returns (uint256 deadAgentsCount) {
        uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]
            storage woundedAgentIdsInRound = woundedAgentIdsPerRound[roundId];
        uint256 woundedAgentIdsCount = woundedAgentIdsInRound[0];
        uint256[] memory woundedAgentIds = new uint256[](woundedAgentIdsCount);
        for (uint256 i; i < woundedAgentIdsCount; ) {
            uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];


            uint256 index = agentIndex(woundedAgentId);
            if (agents[index].status == AgentStatus.Wounded) {
                woundedAgentIds[i] = woundedAgentId;
                _swap({
                    currentAgentIndex: index,
                    lastAgentIndex: currentRoundAgentsAlive - deadAgentsCount,
                    agentId: woundedAgentId,
                    newStatus: AgentStatus.Dead
                });
                unchecked {
                    ++deadAgentsCount;
                }
            }


            unchecked {
                ++i;
            }
        }

Tool used

Manual Review + in-house tool

Recommendation

add some start and end indices to functions.

Duplicate of #28

p-tsanev - Infiltration.sol - VRF re-requesting, opening up rigging possiblity

p-tsanev

medium

Infiltration.sol - VRF re-requesting, opening up rigging possiblity

Summary

VRF randomness could be re-requested, breaking security considerations.

Vulnerability Detail

The protocol makes requests to Chainlink's VRF to request off-chain randomness. Upon a slow response time, > 1 day, a re-request can be and will be triggered, opening up the possibility of provider manipulation. By purposefully withholding the random response, the provider can trigger a re-request of the random number in attempt to get a more favorable value.
Per the VRF docs: Any re-request of randomness is an incorrect use of VRFv2. Doing so would give the VRF service provider the option to withhold a VRF fulfillment if the outcome is not favorable to them and wait for the re-request in the hopes that they get a better outcome
Acceptable risks dispute: The README specifies the VRF provided randomness as trusted, but the issue I am presenting is not related to the randomness itself as discussed with the sponsor - the nodes that provide the inputs that create the random value are the trusted actor. The VRF provider responsible for the callback is a party that has the power to manipulate the outcomes and should not be a trusted actor in the context.
Severity: VRF anti-pattern implementations - historically proven and considered H/M.

Impact

Anti-pattern implementation, game is prone to manipulation and unfair advantages.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L579-L651

Tool used

Manual Review

Recommendation

Get rid of re-request functionality.
Either:

  1. Use a mutation of the previous random number in order to not skew the round
  2. Increase the response time before relying on such functionality
  3. Introduce a back-up source of randomness
    Another resource for tackling randomness on-chain, instead of relying on external randomness:
    https://github.com/paradigmxyz/zk-eth-rng

Duplicate of #64

pontifex - Users have no option to heal agents with probability 99 percents

pontifex

medium

Users have no option to heal agents with probability 99 percents

Summary

Due to check at the line L#844 of the Infiltration contract users have no option to heal agents at the first round after they are wounded. The healing probability can be decreased faster depending on the amount of rounds before the agent dies (ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD). The shortest possible period before an agent dies is 3 rounds. So the probability can decrease dramatically to the second round after wounding.

Vulnerability Detail

Depends on how early the user decided to heal a wounded agent the probability of success of the agent healing is from 99% (the first round since wounding) to 80% (the last round before the agent will be killed):

1004     * Maximum_Heal_Percentage - the maximum % chance a user can heal for, this will be if they heal in Heal_Rounds_Minimum (we have set this to 99% of a successful healing) - this is y1
1005     * Minimum_Heal_Percentage - the minimum % chance a user can heal for, this will be if they heal in Heal_Rounds_Maximum (we have set this to 80% of a successful healing) - this is y2

1019    function healProbability(uint256 healingBlocksDelay) public view returns (uint256 y) {
1020        if (healingBlocksDelay == 0 || healingBlocksDelay > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD) {
1021            revert InvalidHealingBlocksDelay();
1022        }
1023
1024        y =
1025            HEAL_PROBABILITY_MINUEND -
1026            ((healingBlocksDelay * 19) * PROBABILITY_PRECISION) /
1027            ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;
1028    }

The healingBlocksDelay parameter is the amount of rounds since the agent was wounded. So if the amount equals 1 the probability of successful healing is 99%.
But due to the check at the heal function users can't heal agents at the first round after wounding:

844                if (currentRoundId - woundedAt < 2) {
845                    revert HealingMustWaitAtLeastOneRound();
846                }

Impact

Users have no option to heal agents with maximal probability but should have.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L844-L845
https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1014-L1028

Tool used

Manual Review

Recommendation

Consider using 1 instead of 2 in the check:

-               if (currentRoundId - woundedAt < 2) {
+               if (currentRoundId - woundedAt < 1) {
                    revert HealingMustWaitAtLeastOneRound();
                }

Duplicate of #44

y4y - `Infiltration.heal()` doesn't check for agent ownership

y4y

medium

Infiltration.heal() doesn't check for agent ownership

Summary

In the heal() function of the Infiltration contract, user provided agents are not checked for ownership.

Vulnerability Detail

In the heal() function, agents are only checked for their status and length, but not ownership. Any user can heal any agents.

Impact

Despite calling the function costs caller prices and doesn't benefit caller at all, it can still affect the overall game plan/order for other users.

Code Snippet

        for (uint256 i; i < agentIdsCount; ) {
            uint256 agentId = agentIds[i];

            uint256 index = agentIndex(agentId);
            _assertAgentStatus(agents[index], agentId, AgentStatus.Wounded);

            bytes32 agentSlot = _getAgentStorageSlot(index);
            uint256 agentSlotValue;
            uint256 woundedAt;

Tool used

Manual Review

Recommendation

Check for ownership at the beginning of the loop.

Duplicate of #57

rotcivegaf - Hardcoded pool(fee) on contract **InfiltrationPeriphery**

rotcivegaf

medium

Hardcoded pool(fee) on contract InfiltrationPeriphery

Summary

The heal and costToHeal functions of the contract InfiltrationPeriphery interact to V3SwapRouter, both function use the same liquidity pool because the fee of the pool it's constant

Vulnerability Detail

The pool fee is constant 3000(0.3%), which means that the pool tier is always the same: uint24 private constant POOL_FEE = 3_000;

The heal function swap tokens from WETH to LOOKS using always the same pool:

    function heal(uint256[] calldata agentIds) external payable {
        uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);

        IV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({
            tokenIn: WETH,
            tokenOut: LOOKS,
            fee: POOL_FEE,
            recipient: address(this),
            amountOut: costToHealInLOOKS,
            amountInMaximum: msg.value,
            sqrtPriceLimitX96: 0
        });

        uint256 amountIn = SWAP_ROUTER.exactOutputSingle{value: msg.value}(params);

        IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);

        INFILTRATION.heal(agentIds);

        if (msg.value > amountIn) {
            SWAP_ROUTER.refundETH();
            unchecked {
                _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, msg.value - amountIn, gasleft());
            }
        }
    }

The costToHeal function also has this problem although its severity is less since it is a view function:

    function costToHeal(uint256[] calldata agentIds) external returns (uint256 costToHealInETH) {
        uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);

        IQuoterV2.QuoteExactOutputSingleParams memory params = IQuoterV2.QuoteExactOutputSingleParams({
            tokenIn: WETH,
            tokenOut: LOOKS,
            amount: costToHealInLOOKS,
            fee: POOL_FEE,
            sqrtPriceLimitX96: uint160(0)
        });

        (costToHealInETH, , , ) = QUOTER.quoteExactOutputSingle(params);
    }

The pools in UniswaV3 are differentiated between the fee, being able to generate several pools for the same pair, when hardcoded the fee the user is forced to use that single pool

It may be the case that the liquidity of the pool of 3000(0.3%) is changed to the other pool with lower fee, for example 500(0.05%) or 100(0.01%)

Impact

The contract is limited to always using the same pool

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L19

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L48-L58

Tool used

Manual Review

Recommendation

Add fee parameter to the heal and costToHeal functions, with this the user can select the most convenient pool:

@@ -16,8 +16,6 @@ contract InfiltrationPeriphery is LowLevelWETH {
     address public immutable WETH;
     address public immutable LOOKS;
 
-    uint24 private constant POOL_FEE = 3_000;
-
     constructor(
         address _transferManager,
         address _infiltration,
@@ -42,13 +40,13 @@ contract InfiltrationPeriphery is LowLevelWETH {
      * @notice Submits a heal request for the specified agent IDs.
      * @param agentIds The agent IDs to heal.
      */
-    function heal(uint256[] calldata agentIds) external payable {
+    function heal(uint256[] calldata agentIds, uint24 fee) external payable {
         uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);
 
         IV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({
             tokenIn: WETH,
             tokenOut: LOOKS,
-            fee: POOL_FEE,
+            fee: fee,
             recipient: address(this),
             amountOut: costToHealInLOOKS,
             amountInMaximum: msg.value,
@@ -75,14 +73,14 @@ contract InfiltrationPeriphery is LowLevelWETH {
      * @param agentIds The agent IDs to heal.
      * @return costToHealInETH The cost to heal the specified agents.
      */
-    function costToHeal(uint256[] calldata agentIds) external returns (uint256 costToHealInETH) {
+    function costToHeal(uint256[] calldata agentIds, uint24 fee) external returns (uint256 costToHealInETH) {
         uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);
 
         IQuoterV2.QuoteExactOutputSingleParams memory params = IQuoterV2.QuoteExactOutputSingleParams({
             tokenIn: WETH,
             tokenOut: LOOKS,
             amount: costToHealInLOOKS,
-            fee: POOL_FEE,
+            fee: fee,
             sqrtPriceLimitX96: uint160(0)
         });

Duplicate of #6

vampyre - [H-01]Re-requesting randomness from VRF is a security anti-pattern

vampyre

high

[H-01]Re-requesting randomness from VRF is a security anti-pattern

Summary

Re-requesting randomness from VRF is a security anti-pattern as mentioned in the Vrf docs

Likelihood: High, as there is an incentive for a VRF provider to exploit this and it is not hard to do from his side

Vulnerability Detail

The StartNewRound function allows re requesting the randomness from the VRF provider, however this goes against the security standards in using VRF, as stated in the docs:
Any re-request of randomness is an incorrect use of VRFv2. Doing so would give the VRF service provider the option to withhold a VRF fulfillment if the outcome is not favorable to them and wait for the re-request in the hopes that they get a better outcome, similar to the considerations with block confirmation time. Re-requesting randomness is easily detectable on-chain and should be avoided for use cases that want to be considered as using VRFv2 correctly.

Impact

High, as the VRF service provider has control over the random value that provides the best stats for heal and damage, highly impacting the final outcome.
When the startNewRound is invoked, the attacker/service provider could monitor the transaction mempool, then frontrun the _requestForRandomness();
The Fairness of the protocol is tapered with.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L579C1-L580C1

    /**
     * @inheritdoc IInfiltration
     * @dev If Chainlink randomness callback does not come back after 1 day, we can try by calling
     *      startNewRound again.
     */
    function startNewRound() external nonReentrant {
    //
    //
    //
_requestForRandomness();
}

Tool used

Manual Review
Vrf docs

Recommendation

Dont allow recalling for callback from VRF

Duplicate of #64

cergyk - `InfiltrationPeriphery::heal` users can lose surplus of ETH sent to slippage

cergyk

medium

InfiltrationPeriphery::heal users can lose surplus of ETH sent to slippage

Summary

InfiltrationPeriphery::heal enables a user to heal agents using ETH, and helps by swapping the ETH amount provided to LOOKS (which is used to pay for the healing). However even though the maximum amount paid in ETH is bounded by msg.value, a user can pay more than needed, because the slippage parameter is set to zero.

Vulnerability Detail

See the function InfiltrationPeriphery::heal:
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L41-L70

The parameter sqrtPriceLimitX96 is set to zero, which means that ETH/LOOKS price can be very high, and use all of msg.value, even if it was not intended to by the original user (when a user provides surplus eth, it is returned to him by the periphery).
https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L55

In the scenario when a user provides a surplus of ETH to ensure his tx will not revert, a malicious front-runner can take advantage and sandwich the swap.

Impact

A user providing a surplus of ETH during healing with periphery may lose it

Code Snippet

Tool used

Manual Review

Recommendation

Use an alternative price source such as chainlink and check price deviation against it

Duplicate of #89

coffiasd - fulfillRandomWords must not revert

coffiasd

medium

fulfillRandomWords must not revert

Summary

According to chainlink VRF security:

If your fulfillRandomWords() implementation reverts, the VRF service will not attempt to call it a second time. Make sure your contract logic does not revert

Vulnerability Detail

When VRF service invoke fulfillRandomWords , if current healingAgents is bigger than 0 then protocol call _healRequestFulfilled to heal to kill agent

        if (healingAgents != 0) {
            uint256 healedAgents;
            (healedAgents, deadAgentsFromHealing, currentRandomWord) = _healRequestFulfilled(
                currentRoundId,
                currentRoundAgentsAlive,
                currentRandomWord
            ); <---------------------------------------------- invoke here
            unchecked {
                currentRoundAgentsAlive -= deadAgentsFromHealing;
                activeAgents += healedAgents;
                gameInfo.healingAgents = uint16(healingAgents - healedAgents - deadAgentsFromHealing);
            }
        }

If agent is successfully healed 25% of the $LOOKS paid as healing fees are burned
Those burned token are send to target address via blow function \

        _executeERC20DirectTransfer(
            LOOKS,
            0x000000000000000000000000000000000000dEaD,
            _costToHeal(lastHealCount) / 4
        );

Then let's dive into above function

    function _executeERC20DirectTransfer(address currency, address to, uint256 amount) internal {
        if (currency.code.length == 0) {
            revert NotAContract();
        }

        (bool status, bytes memory data) = currency.call(abi.encodeCall(IERC20.transfer, (to, amount)));

        if (!status) {
            revert ERC20TransferFail();
        }

        if (data.length > 0) {
            if (!abi.decode(data, (bool))) {
                revert ERC20TransferFail();
            }
        }
    }

As we can see above function can potentially revert due to certain reasons. Once revert happen the VRF service will not attempt to call it a second time and the game will no longer continue.

Impact

If fulfillRandomWords the VRF server will not attempt to call it a second time

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1362#L1366

        _executeERC20DirectTransfer(
            LOOKS,
            0x000000000000000000000000000000000000dEaD,
            _costToHeal(lastHealCount) / 4
        );
    function _executeERC20DirectTransfer(address currency, address to, uint256 amount) internal {
        if (currency.code.length == 0) {
            revert NotAContract();
        }

        (bool status, bytes memory data) = currency.call(abi.encodeCall(IERC20.transfer, (to, amount)));

        if (!status) {
            revert ERC20TransferFail();
        }

        if (data.length > 0) {
            if (!abi.decode(data, (bool))) {
                revert ERC20TransferFail();
            }
        }
    }

Tool used

Manual Review

Recommendation

Consider simply storing the randomness and taking more complex follow-on actions in separate contract calls made by you

chrisling - Suspicious `premint` function not specified in Audit context

chrisling

medium

Suspicious premint function not specified in Audit context

medium

Summary

There is a premint function that allows owner to skip mint period check and mint before the game starts. In some cases this is fine but the context Q&A mentioned that owner can only perform 3 actions, and this is not one of them.

Vulnerability Detail

The context Q&A clearly stated that owner can only perform 3 actions:

// The only role is the owner and it can do 3 things

// 1. set/extend mint period
// 2. start the game after mint
// 3. withdraw funds from the contract if the game is bricked

Screenshot 2023-11-01 at 9 54 49 PM

It is also not mentioned anywhere in the project's README.md

The premint function allows owner to bypass mint period check and mint the NFTs directly, which is not one of the actions that owner should be able to perform according to context Q&A.

    /**
     * @inheritdoc IInfiltration
     * @notice As long as the game has not started (after mint end), the owner can still mint.
     */
    function premint(address to, uint256 quantity) external payable onlyOwner {
        if (quantity * PRICE != msg.value) {
            revert InsufficientNativeTokensSupplied();
        }

        if (totalSupply() + quantity > MAX_SUPPLY) {
            revert ExceededTotalSupply();
        }

        if (gameInfo.currentRoundId != 0) {
            revert GameAlreadyBegun();
        }

        _mintERC2309(to, quantity);
    }

Impact

Owner role can bypass the rules and mint NFTs before anyone can, which is not one of the actions that owner should be able to perform according to context Q&A.

Code Snippet

https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L449-L464

Tool used

Manual Review

Recommendation

Check with project team whether this is intentional or added by accident. Remove the function if necessary.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.