[Ethernaut] 12. Privacy

Ethernaut · 13/36
  1. 0. Hello Ethernaut
  2. 1. Fallback
  3. 2. Fallout
  4. 3. CoinFlip
  5. 4. Telephone
  6. 5. Token
  7. 6. Delegation
  8. 7. Force
  9. 8. Vault
  10. 9. King
  11. 10. Re-entrancy
  12. 11. Elevator
  13. 12. Privacy
  14. 13. Gatekeeper One
  15. 14. Gatekeeper Two
  16. 15. Naught Coin
  17. 16. Preservation
  18. 17. Recovery
  19. 18. MagicNumber
  20. 19. Alien Codex
  21. 20. Denial
  22. 21. Shop
  23. 22. Dex
  24. 23. Dex Two
  25. 24. Puzzle Wallet
  26. 25. Motorbike
  27. 26. DoubleEntryPoint
  28. 27. Good Samaritan
  29. 28. Gatekeeper Three
  30. 29. Switch
  31. 30. HigherOrder
  32. 31. Stake
  33. 32. Impersonator
  34. 33. Magic Animal Carousel
  35. 34. Bet House
  36. 35. Elliptic Token

1. 문제

 이 컨트랙트의 작성자는 스토리지의 민감한 부분에 대해 충분히 주의를 기울이고 컨트랙트를 작성했습니다.
 이 컨트랙트의 잠금을 해제하시오.

 도움이 될 만한 것들:
 * 스토리지 작동 원리 이해
 * 함수의 파라미터가 어떻게 파싱되는지 이해
 * 타입 캐스팅이 어떻게 동작하는지 이해

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Privacy {

  bool public locked = true;
  uint256 public ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(block.timestamp);
  bytes32[3] private data;

  constructor(bytes32[3] memory _data) {
    data = _data;
  }
  
  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

  /*
    A bunch of super advanced solidity algorithms...

      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
  */
}

2. 해법

 컨트랙트의 잠금을 해제하기 위해서는 스토리지에 저장된 32bytes 타입의 길이가 3인 배열 data의 마지막 원소값이 필요합니다. 우선 블록 탐색기를 열어서 컨트랙트가 배포될 때 스토리지에 어떤 변화가 있었는지 확인합니다.

 여기서 스토리지 슬롯의 다섯 번째 인덱스에 들어있는 값을 사용하겠습니다. bytes32 타입의 값을 더 작은 타입으로 캐스팅하면 값이 뒷부분에서부터 잘려나갑니다. 따라서 bytes16으로 캐스팅한 결과는 0x를 제외하고 앞부분의 16 바이트를 가지게 됩니다. 이 값을 unlock 함수의 인수로 집어넣고 함수를 호출합니다.

 트랙잭션이 컨펌되고나서 locked 값을 확인하면 false로 변경된 것을 확인할 수 있습니다.

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


3. 스토리지

스토리지에 저장되는 값들은 슬롯에 순서대로 저장이 되며, 스토리지의 각 슬롯에는 최대 32바이트 크기의 데이터가 저장될 수 있습니다. 그렇다면 우선적으로 다음과 같은 스토리지 레이아웃을 생각해볼 수 있습니다.

 그런데 이렇게 배치를 하게 되면 31바이트(엄밀히 따지면 255비트) + 31바이트 + 31바이트 + 30바이트해서 총 123바이트가 낭비됨으로 인해 불필요한 비용이 발생하게 됩니다. (일반적인 값 타입의 경우 빈 자리는 00으로 채워짐)

 그래서 Solidity에서는 다음과 같이 연속하는 스토리지 변수들의 크기의 합이 32바이트를 초과하지 않는 선에서 하나의 슬롯에 함께 담습니다. 이 경우에는 낭비되는 메모리가 31바이트 + 28바이트로 크게 줄은 것을 확인할 수 있습니다. 스토리지 레이아웃이 이러하므로, data[2]는 스토리지의 다섯 번째 슬롯에서 읽어올 수 있습니다.


4. 결론

  1. private은 secret이 아니다.
  2. 스토리지가 어떻게 동작하는지 알아보았다.
  3. 바이트열 타입의 값은 작은 타입으로 캐스팅할 때 뒷부분에서 잘려나간다.