Skip to content
This repository has been archived by the owner on May 5, 2024. It is now read-only.

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 #43

Open
sherlock-admin2 opened this issue Nov 4, 2023 · 4 comments
Labels
Has Duplicates A valid issue with 1+ other issues describing the same vulnerability Medium A valid Medium severity issue Reward A payout will be made for this issue Sponsor Disputed The sponsor disputed this issue's validity Won't Fix The sponsor confirmed this issue will not be fixed

Comments

@sherlock-admin2
Copy link
Contributor

sherlock-admin2 commented Nov 4, 2023

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.

@0xhiroshi
Copy link

This is a valid PvP game strategy.

@0xhiroshi 0xhiroshi added the Sponsor Disputed The sponsor disputed this issue's validity label Nov 6, 2023
@nevillehuang
Copy link
Collaborator

nevillehuang commented Nov 7, 2023

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.

@nevillehuang nevillehuang added Medium A valid Medium severity issue and removed High A valid High severity issue labels Nov 7, 2023
@0xhiroshi 0xhiroshi added the Won't Fix The sponsor confirmed this issue will not be fixed label Nov 7, 2023
@0xhiroshi
Copy link

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

@nevillehuang
Copy link
Collaborator

nevillehuang commented Nov 8, 2023

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.

@Evert0x Evert0x removed the Medium A valid Medium severity issue label Nov 9, 2023
@sherlock-admin sherlock-admin changed the title Damaged Ocean Mantaray - 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 - 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 Nov 9, 2023
@sherlock-admin sherlock-admin added Non-Reward This issue will not receive a payout and removed Has Duplicates A valid issue with 1+ other issues describing the same vulnerability labels Nov 9, 2023
@Czar102 Czar102 added the Medium A valid Medium severity issue label Nov 17, 2023
@sherlock-admin sherlock-admin added Reward A payout will be made for this issue and removed Non-Reward This issue will not receive a payout labels Nov 17, 2023
@Czar102 Czar102 reopened this Nov 21, 2023
@sherlock-admin sherlock-admin added the Has Duplicates A valid issue with 1+ other issues describing the same vulnerability label Nov 22, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Has Duplicates A valid issue with 1+ other issues describing the same vulnerability Medium A valid Medium severity issue Reward A payout will be made for this issue Sponsor Disputed The sponsor disputed this issue's validity Won't Fix The sponsor confirmed this issue will not be fixed
Projects
None yet
Development

No branches or pull requests

6 participants