sherlock-2023-10-looksrare-h02
[H-02] Winning agent id may be uninitialized when game is over, locking grand prize
Summary
등수 정보를 저장하는 agents 매핑은 스왑이 일어날 때 비로소 초기화된다. 첫번째 인덱스(1번 에이전트)가 1위를 하면 스왑이 일어나지 않아 해당 에이전트의 agents[1]이 초기화되지 않는다. 이로 인해 1위 리워드를 받지 못하고 토큰이 잠긴다.
Keyword
logic flaw, asset lock, gamefi
Vulnerability
agents 매핑은 배열처럼 사용하며, 게임의 등수를 기록하는 데 사용한다. 처음에는 비어있는 매핑으로 초기값(0)으로 채워져있다. 게임 중 에이전트가 사망/탈출 시 맨 마지막 Active 인덱스와 스왑하여 뒤쪽부터 등수를 채워간다.
즉, agents 매핑의 각 인덱스는 스왑이 발생했을 때 비로소 초기화가 된다.
이때문에 각 에이전트의 배열에서의 위치는 _agentIndexToId 함수를 통해 조회하며, 아직 스왑이 일어나지 않아 agents 매핑에 값이 쓰이지 않은 경우 에이전트의 tokenId(=agentId = agents에서의 초기 index)가 즉 agents 에서의 인덱스가 된다.
function _agentIndexToId(Agent storage agent, uint256 index) private view returns (uint256 agentId) {
agentId = agent.agentId;
agentId = agentId == 0 ? index : agentId; // 아직 agents 매핑에 값이 없어 agent 값이 없다면(agent.agentId 가 0) index 를 리턴한다. 해당 index는 아직 스왑되지 않아 tokenId와 agents 매핑 인덱스가 일치함을 의미한다.
}만약 tokenId 1번 에이전트가 우승을 했다면 한번도 agents[1]은 스왑되지 않을 것이다. 따라서 agents[1].agentId 역시 초기값인 0이 들어있다.
1위가 리워드를 청구하는 함수인 claimGrandPrize 함수이다. _assertAgentOwnership 로 요청자가 해당 에이전트의 소유자인지를 확인하는데, agents[1].agentId가 실제로는 1이어야 하지만, 스왑이 일어나지 않아 0에서 변동되지 않았으므로 소유자로 인식되지 않는다.
function claimGrandPrize() external nonReentrant {
_assertGameOver();
@> uint256 agentId = agents[1].agentId;
@> _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);
}
function _assertAgentOwnership(uint256 agentId) private view {
if (ownerOf(agentId) != msg.sender) {
revert NotAgentOwner();
}
}따라서 1번 에이전트가 우승한 경우 리워드를 받아갈 수 없다.
Impact
1등이 리워드를 받을 수 없다. 토큰이 잠긴다.
Mitigation
agentId가 0인 경우 1번 에이전트가 우승한 것이므로, agentId를 1로 변경해준다.
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);
}tags: bughunting, looksrare, smart contract, solidity, gamefi, nft, logic flaw, asset lock, severity high