티스토리 뷰

Solidity/Hacking

[Ethernaut] 15. Naught Coin

piatoss 2024. 1. 28. 12:00

1. 문제

NaughtCoint은 ERC20 토큰이며 당신은 이미 모든 토큰을 손에 쥐고 있다. 문제는 10년이 지나야 그 토큰들을 사용할 수 있다는 것이다. 시간 제한이 풀리기 전에 다른 주소로 자유롭게 토큰을 보낼 수 있는 방법은 없을까? 당신의 토큰 잔액을 0으로 만들어라.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';

 contract NaughtCoin is ERC20 {

  // string public constant name = 'NaughtCoin';
  // string public constant symbol = '0x0';
  // uint public constant decimals = 18;
  uint public timeLock = block.timestamp + 10 * 365 days;
  uint256 public INITIAL_SUPPLY;
  address public player;

  constructor(address _player) 
  ERC20('NaughtCoin', '0x0') {
    player = _player;
    INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
    // _totalSupply = INITIAL_SUPPLY;
    // _balances[player] = INITIAL_SUPPLY;
    _mint(player, INITIAL_SUPPLY);
    emit Transfer(address(0), player, INITIAL_SUPPLY);
  }
  
  function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
    super.transfer(_to, _value);
  }

  // Prevent the initial owner from transferring tokens until the timelock has passed
  modifier lockTokens() {
    if (msg.sender == player) {
      require(block.timestamp > timeLock);
      _;
    } else {
     _;
    }
  } 
}

2. 해법

function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
  super.transfer(_to, _value);
}

modifier lockTokens() {
  if (msg.sender == player) {
    require(block.timestamp > timeLock);
    _;
  } else {
   _;
  }
}

 컨트랙트를 배포해 놓고 10년을 기다리면 문제를 해결할 수 있기는 한데 그렇게 하기에는 인생은 너무 짧죠. 어떻게서든 지금 당장 잔액을 0으로 만들어야 합니다. 일단 문제가 되는 부분이 바로 lockTokens 변경자인데 if 조건문에서 msg.sender가 player인 경우에만 시간을 검사하고 그렇지 않은 경우는 검사하지 않습니다. player가 transfer 함수를 호출해서 토큰을 보내려면 반드시 10년이 지나야만 합니다. 그런데 한편으로는 나는 토큰을 보낼 수가 없는데 다른 누군가는 마음대로 토큰을 보낼 수 있다는 것입니다.

 

 다른 누군가가 토큰을 마음대로 전송할 수 있더라도 모든 토큰을 내가 들고 있는데 어떡해 해야 될까요? ERC20 표준에는 토큰을 전송하는 방법이 transfer말고도 하나 더 있습니다. 다른 토큰 소유자 A가 자신이 소유한 토큰의 일부를 B가 사용할 수 있도록 approve(address spender, uint256 value) 함수의 호출을 통해 허가를 내려주면, B는 transferFrom(address from, address to, uint256 value) 함수를 사용해 A의 계좌(컨트랙트 내의 상태 변수)로부터 토큰을 자신 또는 또 다른 이에게 전송할 수 있습니다. 이 방법을 사용해 문제를 풀어보겠습니다.

 

 계정을 여러 개 사용하는 방법도 있지만, 컨트랙트를 사용하겠습니다. 컨트랙트는 다음과 같이 작성하였습니다.

contract Withdrawal {
    address public coin;

    constructor(address _coin) {
        coin = _coin;
    }

    function withdraw() public returns(bool) {
        NaughtCoin instance = NaughtCoin(coin);
        uint amount = instance.allowance(msg.sender, address(this));
        return instance.transferFrom(msg.sender, address(this), amount);
    }
}

 Remix IDE에서 컨트랙트를 배포합니다.

 

 컨트랙트가 배포되고 나면, 컨트랙트 주소를 대상으로 현재 가지고 있는 모든 토큰에 대한 임시 권한을 부여합니다.

 

 트랜잭션이 컨펌되고 나면, 배포한 컨트랙트의 withdraw 함수를 호출하여 자신의 계정에 있는 모든 토큰을 컨트랙트로 옮깁니다.

 

 트랜잭션이 컨펌되고 자신의 계정에 들어있는 잔액을 확인하면 0이 된 것을 확인할 수 있습니다.

 

 인스턴스를 제출하고 마무리합니다.


3. 결론

안전한 라이브러리(Open)를 사용한다고 해서 임의로 추가한 로직까지 안전한 것은 아니다!

 

 

ERC 20 - OpenZeppelin Docs

In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through frontrunning with a "donation" to the vault that inflates the price of a share. This is variously known as a donation or inflation attack and is essentially a pro

docs.openzeppelin.com

 

'Solidity > Hacking' 카테고리의 다른 글

[Ethernaut] 17. Recovery  (1) 2024.01.30
[Ethernaut] 16. Preservation  (0) 2024.01.29
[Ethernaut] 14. Gatekeeper Two  (0) 2024.01.27
[Ethernaut] 13. Gatekeeper One  (1) 2024.01.25
[Ethernaut] 12. Privacy  (1) 2024.01.24
최근에 올라온 글
최근에 달린 댓글
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Total
Today
Yesterday
글 보관함