sherlock-2023-10-looksrare-m03
[M-03] Index values selected in _woundRequestFulfilled() are not uniformly distributed
Summary
_woundRequestFulfilled 함수에서 이번 라운드에 부상당할 에이전트를 랜덤으로 뽑을 때, 이 확률이 균일하게 랜덤으로 분포되지 않는다. 부상당할 에이전트 바로 옆에 있는 인덱스가 선택될 가능성이 높기 때문에 부상당한 에이전트의 분포가 편향될 수 있다.
Keyword
insecure randomness, unfairness, gamefi
Vulnerability
_woundRequestFulfilled 함수에서, 랜덤으로 뽑은 인덱스에 이미 부상당한 에이전트가 있다면 랜덤 숫자를 새로 뽑지 않고 바로 다음 인덱스를 선택한다.
function _woundRequestFulfilled(
uint256 roundId,
uint256 currentRoundAgentsAlive,
uint256 activeAgents,
uint256 randomWord
) private returns (uint256 woundedAgentsCount) {
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;
}
uint256[] memory woundedAgentIds = new uint256[](woundedAgentsCount);
uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]
storage currentRoundWoundedAgentIds = woundedAgentIdsPerRound[roundId];
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;
...
@> 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;
}
}
}
...
}8명의 에이전트에서 랜덤으로 뽑는 상황을 생각해보자. 처음에는 모두가 동일한 확률로 뽑힌다.
| 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 |
인덱스 2의 에이전트가 부상당하면 인덱스 3의 에이전트가 뽑힐 확률이 2배가 된다. (인덱스를 2 또는 3을 뽑았을 때 뽑히기 때문)
| 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 |
인덱스 2와 3이 부상당했다면, 인덱스 4가 뽑힐 확률은 3배가 된다. (인덱스를 2, 3, 4를 뽑았을 때 뽑히기 때문)
| 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
부상 확률이 균일하지 않다. 부상당한 에이전트의 분포에 불공평함이 생긴다.
Mitigation
매번 새로운 랜덤값을 생성해 사용한다. (_nextRandomWord는 해시를 통해 충분한 랜덤성 제공)
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);tags: bughunting, looksrare, smart contract, solidity, gamefi, nft, insecure randomness, unfairness, severity medium