code4rena-2024-01-salty-h01

[H-01] Development Team might receive less SALT because there is no access control on VestingWallet_release

보고서

Summary

누구나 VestingWallet.relase를 호출할 수 있으므로 이를 Upkeep.performUpkeep를 통하지 않고 직접 호출하면 개발팀의 토큰이 Upkeep 컨트랙트에 잠긴다.

Keyword

access control, vesting wallet

Vulnerability

런칭 시 Team vesting wallet 에게 1000만개의 SALT 토큰을 예치한다. 이는 10년간 선형적으로 꺼내어 개발팀에게 전달된다. Upkeep.performUpkeep 를 호출하면 VestingWallet.relase를 호출하여 Upkeep 컨트랙트가 꺼내진 토큰을 받는다. 그리고 이를 개발팀의 월렛에 전달한다.

  function step11() public onlySameContract
{
    uint256 releaseableAmount = VestingWallet(payable(exchangeConfig.teamVestingWallet())).releasable(address(salt));
      
    // teamVestingWallet actually sends the vested SALT to this contract - which will then need to be sent to the active teamWallet
@>  VestingWallet(payable(exchangeConfig.teamVestingWallet())).release(address(salt));
      
@>  salt.safeTransfer( exchangeConfig.managedTeamWallet().mainWallet(), releaseableAmount );
}

배포 스크립트를 보면 VestingWallet 컨트랙트는 Openzeppelin 것을 사용한다. 첫번쨰 파라미터가 address(upkeep) 로 설정되는데, 이는 VestingWallet의 beneficiary 가 Upkeep 컨트랙트로 설정됨을 의미한다.

  teamVestingWallet = new VestingWallet( address(upkeep), uint64(block.timestamp), 60 * 60 * 24 * 365 * 10 );

VestingWallet.relase는 누구나 호출할 수 있지만 토큰은 Upkeep에게 토큰이 전달된다.

  // OpenZeppelin VestingWallet.relase
  function release() public virtual {
    uint256 amount = releasable();
    _released += amount;
    emit EtherReleased(amount);
@>  Address.sendValue(payable(beneficiary()), amount);
  }

그런데 문제가 있다. Upkeep.performUpkeep에 의해 VestingWallet.relase 를 호출하면 토큰을 바로 개발팀의 월렛으로 전달하지만 누군가가 직접 VestingWallet.relase를 호출하면 Upkeep으로 토큰이 전송되더라도 이를 개발팀의 월렛으로 전달하지 못한다. 이 토큰은 영원히 Upkeep 컨트랙트에 잠기게 된다.

Impact

개발팀이 토큰을 받을 수 없음

Mitigation

Upkeep에서 직접 VestingWallet에서 풀린 토큰을 받아 넘기지 않고 풀린 토큰을 수령하는 컨트랙트를 하나 더 낀다. (Upkeep 중개 컨트랙트 VestingWallet) 중개 컨트랙트에 예치된 토큰은 개발팀이 언제든 꺼내갈 수 있도록 한다.


tags: bughunting, saltyio, smart contract, solidity, solo issue, access control vulnerability, vesting wallet, severity high