BitsLab AI / Product guides

Example finding: unrestricted treasury withdrawal

An educational report example showing the evidence a reviewer should expect. This hand-written fixture is not a customer audit, a scanner-generated result or a claim that BitsLab AI has executed these tests.

Scope and security property

Identifier: EXAMPLE-001. Affected function: VulnerableTreasury.withdraw. Impact under the example assumptions: any account can transfer the treasury's ETH to an arbitrary recipient.

The intended rule is that only the owner can withdraw. This toy contract has no proxy, token accounting or external protocol dependencies. The example does not evaluate reentrancy or broader production risks.

Evidence chain

  1. A treasury starts with 1 ETH.
  2. An account with no authorized role calls withdraw with itself as the recipient.
  3. The function checks no caller permission before making the transfer.
  4. The test asserts that the caller receives 1 ETH and the treasury balance reaches zero.

Inspectable code and validation tests

The fixture includes the vulnerable contract, an owner-restricted version and three Foundry tests. The first demonstrates the violation; the other two check rejection of an unauthorized caller and preservation of the owner's intended access.

Download AccessControlExample.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Save as test/AccessControlExample.t.sol in a Foundry project.
// Run: forge test --match-contract AccessControlExampleTest -vv
interface Vm {
    function deal(address account, uint256 balance) external;
    function prank(address sender) external;
    function expectRevert(bytes4 selector) external;
}

contract VulnerableTreasury {
    constructor() payable {}

    function withdraw(address payable recipient, uint256 amount) external {
        (bool ok,) = recipient.call{value: amount}("");
        require(ok, "transfer failed");
    }
}

contract FixedTreasury {
    error Unauthorized();
    address public immutable owner;

    constructor() payable { owner = msg.sender; }

    function withdraw(address payable recipient, uint256 amount) external {
        if (msg.sender != owner) revert Unauthorized();
        (bool ok,) = recipient.call{value: amount}("");
        require(ok, "transfer failed");
    }
}

contract AccessControlExampleTest {
    Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
    address payable constant attacker = payable(address(0xBEEF));

    function testDemonstratesUnauthorizedWithdrawal() public {
        vm.deal(address(this), 1 ether);
        vm.deal(attacker, 0);
        VulnerableTreasury treasury = new VulnerableTreasury{value: 1 ether}();
        vm.prank(attacker);
        treasury.withdraw(attacker, 1 ether);
        require(attacker.balance == 1 ether, "unauthorized withdrawal reproduced");
        require(address(treasury).balance == 0, "treasury drained");
    }

    function testFixedBlocksUnauthorizedWithdrawal() public {
        vm.deal(address(this), 1 ether);
        FixedTreasury treasury = new FixedTreasury{value: 1 ether}();
        vm.expectRevert(FixedTreasury.Unauthorized.selector);
        vm.prank(attacker);
        treasury.withdraw(attacker, 1 ether);
        require(address(treasury).balance == 1 ether, "treasury retains funds");
    }

    function testFixedAllowsOwnerWithdrawal() public {
        vm.deal(address(this), 1 ether);
        vm.deal(attacker, 0);
        FixedTreasury treasury = new FixedTreasury{value: 1 ether}();
        treasury.withdraw(attacker, 1 ether);
        require(attacker.balance == 1 ether, "authorized withdrawal works");
    }
}

Reproduce and record results

In a Foundry project configured for Solidity 0.8.20 or newer, save the fixture as test/AccessControlExample.t.sol, then run:

forge test --match-contract AccessControlExampleTest -vv

The tests are designed to reproduce the unauthorized transfer in the vulnerable version, reject it in the fixed version and allow an owner transfer. Record your compiler version, command and actual execution output. Expected behavior is not a substitute for an execution log.

Remediation and follow-up

Enforce the intended authorization rule before transferring assets. Rerun both the exploit regression test and legitimate-use tests. In a real protocol, also review ownership transfer, role administration, external calls and integration assumptions.

For an actual scan, review its generated findings, evidence and validation status in the dashboard. Read how to interpret PoC validation before treating a finding as confirmed.

Explore BitsLab AI

BitsLab AI is BitsLab's AI smart contract security scanner. Explore how structured analysis, evidence chains and PoC validation support security review.

Read the full BitsLab AI FAQ on the BitsLab website