Level 1은 Fallback이라는 이름의 문제이다.
Fallback이란?
어떤 기능이 약해지거나 제대로 동작하지 않을 때, 이에 대처하는 기능 또는 동작
스마트 컨트랙트에는 Fallback 함수가 있다.
fallback함수는 무기명 함수이다.
직접 호출되지 않고, 특정 상황에서 제대로 동작하지 않을 때 디폴트로 fallback함수가 실행된다.
컨트랙트에서는 호출한 함수가 컨트랙트 내에서 조회되지 않는 경우(주소가 확인되지 않는 경우)가 있다.
또한 Ether를 보낼 때도 자동으로 실행된다.
Reference
https://velog.io/@octo__/Solidity-fallback-function
이 레벨을 통과하기 위해서는 다음 두 가지 조건이 필요하다.
1. contract의 ownership를 선언한다.(내가 owner가 되고자 한다.)
2. 계좌의 잔액을 0으로 만든다.
아래는 컨트랙트 코드이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Fallback {
mapping(address => uint256) public contributions;
address public owner;
constructor() {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint256) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
이 레벨을 풀 때 도움이 될 지도 모르는 힌트들
1. ABI와 상호작용할 때 어떻게 이더를 보낼까?
2. ABI 외부로 어떻게 이더를 보낼까?
3. help() 커맨드로 wei/ether 전환
4. Fallback 함수
먼저, 코드를 살펴보자.
owner를 msg.sender로 설정, 즉 컨트랙트 배포자(트랜잭션을 보낸 사람)의 주소를 소유자로 설정했다.
contributions[msg.sender] = 1000 * (1 ether)로, 소유자의 초기 기여도(?)를 1000 ETH로 설정했다.
modifier를 사용해 함수 호출자가 소유자인지 확인한다.
require함수로 트랜잭션을 보낸 사람의 주소가 컨트랙트의 소유자 주소와 동일한지 확인한다. 같지 않으면 "caller is not the owner" 메세지를 출력 후 트랜잭션을 실패하게 한다.
_;는 modifier가 적용된 함수 본문의 코드가 실행된다.
function withdraw() public onlyOwner 에서 owner만이 해당 코드(payable(owner).transfer(address(this).balance);)를 실행할 수 있다.
공개적으로 호출 가능, payable 키워드를 사용했으므로 ETH를 받을 수 있다.
함수 호출자가 보내는 이더의 양(msg.value)가 0.001 ETH보다 적어야 한다. 이보다 많으면 트랜잭션이 실패한다.
if문에서는 기여자가 보낸 이더의 총합이 현재 소유자가 보낸 이더의 양보다 클 경우, 기여자를 새 소유자(owner)로 설정한다.
*즉, 기여도가 가장 큰 사람 = owner
공개로, view로 표시되어 데이터를 조회하는 함수이다.(상태 변경 x)
현재 호출자 주소에 대한 기여도를 반환하여 사용자는 자신의 기여도를 이 함수로 확인가능하다.
modifier로 설정된 onlyOwner 조건을 만족하는 경우에만 해당 함수가 실행된다. 즉, owner만 호출할 수 있다.
payable이므로 이더를 받을 수 있음을 의미하며, 이 컨트랙트가 보유한 잔액을 transfer 함수로 owner에게 모두 전송한다.
즉, 소유자가 컨트랙트의 모든 자금을 인출할 수 있는 기능이다.
function 키워드를 사용하지 않는 특별한 함수로, receive라는 예약된 이름을 사용해 선언한다.
스마트 컨트랙트가 직접 이더를 전송받을 때 호출된다.(자동 호출)
external이므로 외부에서만 호출 가능하며, 이더 수신이 가능하다.
함수 호출자의 기여도가 0보다 높고 사용자가 0 이상의 이더를 전송해야만 실행되며, 이더를 보낸 사용자의 주소를 owner로 변경한다.
contract.owner()키워드로 주소 확인을 해보면,
0x3c99F231E92c4F0009aC726dd310Bd76d1c755bB
(이후에 바뀌므로 잠시만 기억해놓자)
contract.contribute({value:toWei("0.0000000000000001")})
1Wei를 보내보았다.

를 해보니
인스턴스 주소로 1Wei가 전송된 것을 확인할 수 있었다.
전송했으니 내가 owner이다.
코드를 다시 보면 0.001 이더 미만을 보내고, 컨트랙트로 0 이상의 이더를 보내야 한다.
즉 아직 owner가 되지 않은 상태이다.
contract.owner()을 해보면 아직 내 주소로 변경되지 않았다.
다시 0.0005 ETH를 보내고(1Wei는 너무 적어서 조금 더 보내보았다), 컨펌시킨 후 컨트랙트로 0.0001 이더를 보내보자.
help()를 보면 sendTransaction({options})가 있다.
활용하여, contract.sendTransaction({value:toWei("0.0001")})
컨펌되었으면, contract.owner()을 다시 해보자.

내가 owner가 되었다!!
이제 contract.withdraw()로 컨트랙트의 이더를 0으로 만들자.

성공!