이 프록시 지갑의 admin(관리자) 이 되어서 지갑을 탈취해야 한다.
요즘 DeFi(탈중앙화 금융)에서 무언가를 하려면 수수료가 너무 비싸서 사실상 불가능하다.
그래서 친구 몇 명이 여러 트랜잭션을 하나로 묶어 처리(batch) 하면 약간이나마 비용을 줄일 수 있다는 사실을 발견했다.
이들은 이를 위해 트랜잭션을 한 번에 처리하는 스마트 컨트랙트를 개발했다.
하지만 혹시라도 코드에 버그가 있을 경우를 대비해 이 스마트 컨트랙트가 업그레이드 가능해야 했고,
또한 그들 그룹 외의 사람들은 이 컨트랙트를 사용하지 못하게 하고 싶었다.
그래서 이들은 투표를 통해 시스템에 특별한 권한을 가진 두 사람을 지정했다:
• Admin (관리자): 스마트 컨트랙트의 로직을 업그레이드할 수 있는 권한을 가짐
• Owner (소유자): 컨트랙트를 사용할 수 있는 허용된 주소 목록(화이트리스트) 을 관리할 수 있음
그렇게 컨트랙트는 배포되었고, 그룹 구성원들은 화이트리스트에 등록되었다.
모두가 악덕 채굴자들에 맞서 자신들의 성취를 자축했다.
하지만…
그들은 몰랐다.
그들의 점심값이 위험에 처해 있다는 사실을…
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import "./UpgradeableProxy.sol";
contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;
constructor(address _admin, address _implementation, bytes memory _initData)
UpgradeableProxy(_implementation, _initData)
{
admin = _admin;
}
modifier onlyAdmin() {
require(msg.sender == admin, "Caller is not the admin");
_;
}
function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}
function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
admin = pendingAdmin;
}
function upgradeTo(address _newImplementation) external onlyAdmin {
_upgradeTo(_newImplementation);
}
}
contract PuzzleWallet {
address public owner;
uint256 public maxBalance;
mapping(address => bool) public whitelisted;
mapping(address => uint256) public balances;
function init(uint256 _maxBalance) public {
require(maxBalance == 0, "Already initialized");
maxBalance = _maxBalance;
owner = msg.sender;
}
modifier onlyWhitelisted() {
require(whitelisted[msg.sender], "Not whitelisted");
_;
}
function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}
function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}
function deposit() external payable onlyWhitelisted {
require(address(this).balance <= maxBalance, "Max balance reached");
balances[msg.sender] += msg.value;
}
function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] -= value;
(bool success,) = to.call{value: value}(data);
require(success, "Execution failed");
}
function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success,) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}
https://github.com/OpenZeppelin/ethernaut/blob/master/contracts/src/helpers/UpgradeableProxy-08.sol
스마트 컨트랙트는 한 번 배포하면 코드를 수정할 수 없습니다. 이는 블록체인의 “불변성” 특성에 기인한 것으로, 투명성과 신뢰성 확보에 중요한 역할을 합니다. 하지만 이러한 특성은 동시에 취약점이 발견되었을 때 심각한 보안 리스크로 이어질 수 있습니다. 이미 배포된 컨트랙트에서 보안 결함이 발견되면, 그 컨트랙트는 사실상 무방비로 공격에 노출될 수밖에 없습니다.
이런 상황에 대한 대응책으로 가장 널리 사용되는 방식은 두 가지입니다.
그러나 두 방법 모두 한계가 있습니다. Pausable은 일시적 대응에 불과하며, 새로운 컨트랙트 배포 후 마이그레이션은 사용자 자산, 상태 변수, 계약 연동 상태 등을 모두 옮겨야 하므로 비용과 복잡성이 상당히 큽니다.
이러한 문제를 해결하기 위해 등장한 것이 바로 ERC-1967 기반의 Proxy 패턴입니다.