A self-contained demonstration of a smart-contract security review, written in Vyper: a
deliberately vulnerable vault whose set_owner ships without the owner guard its sibling
admin functions carry, the ownership takeover and drain it allows (proven with a passing
Foundry proof-of-concept), and a fixed branch where the
same scenario is neutralised.
This is a demonstration on intentionally vulnerable code.
Vault.vywas written to showcase audit methodology end to end. It is not production code, not a real client engagement, and must never be deployed. The finding is a real vulnerability in this demo contract, not an invented severity.
Anyone can write "I audit smart contracts" in a bio. This repo shows the work instead: a target, a concrete finding, an executable proof, and a verified fix. If it isn't reproducible, it isn't done.
The review lives across two branches:
| Branch | Contents | What a green forge test means |
|---|---|---|
master |
The vulnerable vault and the PoC that exploits it | any account seizes ownership and drains the balance |
fixed |
The remediated vault and the same scenario | the takeover reverts and the balance stays put |
src/Vault.vy, the contract under review (Vyper 0.4.3)test/Vault.poc.t.sol, the proof-of-concept (Foundry, Solidity harness over the Vyper contract)Vyper_Access_Control_Review.pdf, the full written report
| ID | Severity | Summary |
|---|---|---|
| H-01 | High | Unguarded ownership transfer. set_owner is missing the assert msg.sender == self.owner check that pause and withdraw_all both enforce. An attacker calls set_owner(attacker), becomes owner, then calls the legitimately guarded withdraw_all to take the funds. The asymmetry between the sibling functions is the bug. |
PoC numbers on master: a victim deposits 10 ether. The attacker calls set_owner to seize
ownership, then withdraw_all, and walks away with the full 10 ether. On fixed the takeover
reverts, ownership holds, and the vault keeps its 10 ether.
Requires Foundry and the
Vyper 0.4.x compiler on PATH (pip install vyper), which
Foundry invokes to compile the .vy contract.
git clone https://github.com/Musyg/vyper-access-control-audit.git
cd vyper-access-control-audit
forge install
# master: the takeover and drain succeed
forge test -vv
# fixed: the same attack reverts
git checkout fixed
forge test -vvAdd the owner guard to set_owner so it matches its siblings:
@external
def set_owner(new_owner: address):
assert msg.sender == self.owner, "not owner"
self.owner = new_ownerThe rule is mechanical: every state-changing admin entry point must enforce the same access check. When four of five sibling functions carry a guard, the unguarded outlier is the finding.
High: an unprivileged actor takes full control of the contract and drains 100% of the balance. The only precondition is the vault holding funds, which is its normal state.