code4rena-2021-06-pooltogether-h04

[H-04] withdraw timelock can be circumvented

보고서

Summary

withdrawWithTimelockFrom를 중복으로 호출할 시 Timelock을 덮어쓴다. 다량의 토큰을 출금 요청한 뒤 0만큼 출금 요청하면 Timelock 종료 시간이 현재로 덮어써져 바로 출금이 가능하다.

Keyword

bug, logic flaw, timelock

Vulnerability

PrizePool에서 Timelock을 우회하여 출금이 가능하다. withdrawWithTimelockFrom 호출 시 기존 Timelock을 덮어쓴다. 따라서 먼저 다량의 토큰을 출금 요청하고, 다시 소량 또는 0만큼 출금 요청을 하면 Timelock이 덮어써진다. 따라서 기한이 다 끝날 때까지 기다리지 않고 토큰을 출금해갈 수 있다.

  function withdrawWithTimelockFrom(
    address from,
    uint256 amount,
    address controlledToken
  )
    external override
    nonReentrant
    onlyControlledToken(controlledToken)
    returns (uint256)
  {
    uint256 blockTime = _currentTime();
    (uint256 lockDuration, uint256 burnedCredit) = _calculateTimelockDuration(from, controlledToken, amount);
    uint256 unlockTimestamp = blockTime.add(lockDuration);
    _burnCredit(from, controlledToken, burnedCredit);
    ControlledToken(controlledToken).controllerBurnFrom(_msgSender(), from, amount);
    _mintTimelock(from, amount, unlockTimestamp);
    emit TimelockedWithdrawal(_msgSender(), from, controlledToken, amount, unlockTimestamp);
 
    // return the block at which the funds will be available
    return unlockTimestamp;
  }
 
  function _mintTimelock(address user, uint256 amount, uint256 timestamp) internal {
    // Sweep the old balance, if any
    address[] memory users = new address[](1);
    users[0] = user;
    _sweepTimelockBalances(users);
 
    timelockTotalSupply = timelockTotalSupply.add(amount);
    _timelockBalances[user] = _timelockBalances[user].add(amount);
    _unlockTimestamps[user] = timestamp;
 
    // if the funds should already be unlocked
    if (timestamp <= _currentTime()) {
      _sweepTimelockBalances(users);
    }
  }
  1. withdrawWithTimelockFrom(user, amount=userBalance) 를 호출하여 다량의 토큰에 Timelock을 건다.
  2. withdrawWithTimelockFrom(user, amount=0)를 호출하여 0만큼 출금 신청을 한다. 이렇게 하면 lockDuration이 0이 되고, unlockTimestamp = blockTime.add(0); 이 되어 현재 시간이 된다.
  3. _mintTimelock 함수에서 _unlockTimestamps[user] = timestamp;로 덮어써져 Timelock이 해제된다.
  4. sweepTimelockBalances 함수를 호출해 토큰을 바로 꺼내간다.

Impact

패널티 없이 출금이 가능하다. 공격자가 복권 추첨 직전에 참여했다가 패널티 없이 바로 나올 수 있으므로 프로토콜에 심각한 이슈이다.

Mitigation

withdrawWithTimelockFrom 중복 호출을 막았다. withdrawWithTimelockBalance require timelockBalance gt zerp


tags: bughunting, pooltogether, smart contract, solidity, logic flaw, timelock, severity high