sherlock-2023-10-looksrare-m01
[M-01] 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
유저들의 실행 순서에 따라 부상당한 에이전트가 마지막 치료 기회를 잃을 수 있다.
Keyword
business logic vul, unfairness, gamefi
Vulnerability
각 라운드에서 에이전트는 _requestForRandomness 가 호출되기 전에 에이전트를 치유하거나 탈출할 기회가 있다. heal/escape 함수는 startNewRound 직전이라면 언제든 실행할 수 있으며, 일반적으로 이는 문제되지 않는다.
하지만 게임 내 Active 에이전트가 몇 명밖에 남지 않은 경우 문제가 발생한다.
heal 함수를 호출하기 위해서는 gameInfo.activeAgents가 NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS 보다 커야 한다. 즉, 50명보다 많아야 한다.
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();
}반면 escape 함수는 gameInfo.activeAgents의 수를 줄이며, 에이전트의 상태를 ESCAPE 로 설정한다.
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);
// }따라서 heal 함수가 먼저 호출된다면 해당 부상당한 에이전트는 fulfillRandomWords 에서 치료될 수 있다. 하지만 escape가 먼저 호출되어 Info.activeAgents의 수가 NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS보다 작아진다면 이후 heal 함수가 비활성화 된다.
실행 순서에 따라 결과가 달라지므로(누구는 이번 라운드에서 치료요청이 가능했는데 누구는 못 하게 됨) 이는 게임의 공정성에 영향을 미친다.
Impact
일부 Active 에이전트가 탈출을 선택해 활성 에이전트 수가 NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS 보다 작아지면 부상당한 에이전트를 치료할 마지막 기회를 잃게 된다. 다른 에이전트의 탈출로 인해 치료의 기회를 박탈당하게 된다.
Mitigation
escape 함수는 모든 라운드에서 heal 함수 이후에 처리하도록 한다. 모든 부상당한 에이전트가 스스로 치료할 기회를 가진 후 탈출 처리를 한다.
Memo
이 게임이 PVP 게임인 만큼, 하나의 게임 전략으로 취급될 수 있다며 프로젝트측은 동의하지 않았다. Invalid로 판정하겠다 했는데, 결과적으로 Medium으로 판정되었다.
tags: bughunting, looksrare, smart contract, solidity, gamefi, nft, business-logic-vul, unfairness, severity medium