Back to blogs
Written by
Hans
Published on
June 25, 2025

Solodit Checklist Explained (10): Rug Pull

Learn how unchecked admin privileges enable rug pulls in DeFi. Discover secure design patterns like fee separation, time locks, and restricted withdrawal functions.

Table of Contents

This article examines a critical aspect of smart contract security: the risk of having administrator privileges that allow for the draining of user funds, commonly referred to as a "rug pull."

This is part of the "Solodit Checklist Explained" series. You can find the previous articles here:

For the best experience, open a tab with the Solodit checklist to refer to it as you read.

Examples are available on my GitHub here.

What is a rug pull?

Literally, a "rug pull" is the act of abruptly pulling a rug from under someone, causing them to fall unexpectedly.

In decentralized finance (DeFi), a rug pull refers to a scam in which developers promote a new token or platform, only to suddenly drain the liquidity pool (LP) or sell their holdings in large quantities, causing the token’s value to plummet.

This is a form of "centralization risk." Centralization risk encompasses a wide range of vulnerabilities, such as a single entity controlling upgrades, pausing functions, or critical parameters. This article will focus specifically on fund-draining rug pulls, where administrators exploit their privileges to siphon funds deposited by users or held within the protocol

Other types of centralization risks are addressed comprehensively in a separate category of the Solodit checklist, covering issues like pause switch control and parameter modification capabilities. Here, we focus solely on the direct theft of value through admin-controlled withdrawal functions.

Effective management of administrative access goes beyond writing secure code. It involves designing trust into the protocol.

SOL-AM-RP-1: Can the admin of the protocol pull assets from the protocol?

  • Description: Some protocols grant an admin the privilege of directly withdrawing assets. Generally, any actor that can directly affect user funds must be scrutinized.

  • Remediation: Restrict access to only relevant parts of protocol funds, perhaps by tracking fees internally. Enforcing a time lock on admin actions can also mitigate the risk.

This checklist item addresses a critical risk: an administrator's ability to withdraw assets, which can directly jeopardize user funds

Administrative roles are essential for maintaining and upgrading protocols, but granting unrestricted access to assets creates a significant vulnerability. If an attacker gains control of the admin key, they can drain funds, effectively executing a "rug pull" by depleting the protocol's reserves. Even without malicious intent, a compromised key due to a security breach can lead to severe consequences, undermining the protocol’s reputation and user trust.

To mitigate this risk, smart contracts should be designed to minimize the impact of a compromised or malicious admin account. This involves limiting administrative privileges, enforcing strict checks on all admin actions, and implementing safeguards to prevent unilateral or immediate asset transfers. Administrative functions should be treated like a precise surgical procedure, necessary but carefully restricted to a secure, predefined scope.

A recent example of this vulnerability is the Zunami Protocol incident in May 2025, which resulted in a loss of $500,000 in zunUSD and zunETH collateral. According to rekt.news, this was not a sophisticated exploit involving flash loans or price manipulation. Rather, it was a simple abuse of an overly powerful admin function. An individual with "god-mode access" invoked the withdrawStuckToken() function, emptying the vault’s contents.

This incident highlights that some of the most damaging "hacks" result from authorized administrators, whether malicious or compromised, misusing legitimate contract functions. The Zunami case reminds us that unsecured emergency functions can become tools for attackers if not properly protected.

Here's an example showing how a malicious or compromised admin could drain a vault, execute a rug pull:

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


contract VulnerableVault {
    address public admin;
    // Maps user addresses to their deposited balances
    mapping(address => uint256) public userBalances;


    constructor() {
        // The deployer of the contract becomes the admin
        admin = msg.sender;
    }


    /// @notice Allows users to deposit Ether into the vault.
    /// A 1% fee is applied to the deposit and sent to a fee pool.
    function deposit() public payable {
        require(msg.value > 0, "Deposit must be greater than zero");


        // Calculate 1% fee. This fee theoretically belongs to the protocol.
        uint256 fee = msg.value / 100; // 1% of deposited amount
        uint256 amountToDeposit = msg.value - fee;


        // User's balance is updated with their net deposit.
        userBalances[msg.sender] += amountToDeposit;
        // The 'fee' part of msg.value remains in the contract's total balance.
        // It is not explicitly tracked in a separate variable, which is a design flaw
        // if only fees should be withdrawable by admin.
    }


    /// @notice Allows users to withdraw their deposited funds.
    /// @param amount The amount to withdraw.
    function withdraw(uint256 amount) public {
        require(userBalances[msg.sender] >= amount, "Insufficient balance");


        // Decrement user's balance and transfer funds.
        userBalances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }


    /// @notice Admin can withdraw *any* amount from the contract's balance.
    /// This is the rug pull vulnerability! This simulates a `withdrawStuckToken()`-like function.
    /// @param amount The amount to withdraw from the contract.
    function adminRugPullWithdraw(uint256 amount) public {
        require(msg.sender == admin, "Admin control required"); // Only admin can call.


        // NO checks to ensure 'amount' is only from accrued fees.
        // The admin can withdraw any amount up to the contract's total Ether balance (address(this).balance),
        // effectively stealing user deposits, as user deposits also contribute to address(this).balance.
        // This function treats all funds in the contract as if they are available for the admin to take.
        require(address(this).balance >= amount, "Insufficient contract balance for withdrawal");
        payable(admin).transfer(amount); // Transfer chosen amount to admin.
    }
}


In this VulnerableVault contract, the adminRugPullWithdraw function allows the admin to withdraw any amount of Ether from the contract's total balance. This is a critical vulnerability. If the admin key is compromised, or if the individual controlling the key decides to act maliciously (as was questioned in the Zunami case regarding an "insider job"), they could use this function to drain all the Ether deposited in the vault, including user funds.

The require statement only validates that the caller is the admin, completely failing to validate the amount against what should legitimately be withdrawable (e.g., only protocol fees, not user principal).

To mitigate this vulnerability and prevent a rug pull, consider these robust strategies:

  • Limit admin access to specific funds: Restrict any admin withdrawal function to operate only on protocol-owned funds (e.g., collected fees, treasury funds) and never on user deposits. User deposits should only be withdrawable by the users themselves.

  • Implement time locks: For any sensitive administrative action, especially those involving the movement of funds or critical parameter changes, require a time lock. This delay provides users and monitoring systems with a crucial window to detect potentially malicious transactions and react (e.g., by withdrawing their funds or raising alarms) before the action is finalized. This could have been a critical mitigating factor in the Zunami case, providing a defense against the immediate draining.

Here's a revised version of the contract, mitigating the vulnerability by introducing internal fee tracking and restricting admin withdrawals to only those accrued fees:

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


contract SecuredVault {
    address public admin;
    // Maps user addresses to their deposited balances
    mapping(address => uint256) public userBalances;
    // This variable *only* accounts for fees that the admin can withdraw.
    // It's explicitly separated from user balances.
    uint256 public totalAccruedFees;


    constructor() {
        // The deployer of the contract becomes the admin
        admin = msg.sender;
    }


    /// @notice Allows users to deposit Ether into the vault.
    /// A 1% fee is applied to the deposit and sent to a fee pool.
    function deposit() public payable {
        require(msg.value > 0, "Deposit must be greater than zero");


        // Calculate 1% fee
        uint256 fee = msg.value / 100; // 1% of deposited amount
        uint256 amountToDeposit = msg.value - fee;


        userBalances[msg.sender] += amountToDeposit; // User's principal
        totalAccruedFees += fee; // Accrue the fee for the protocol
    }


    /// @notice Allows users to withdraw their deposited funds.
    /// @param amount The amount to withdraw.
    function withdraw(uint256 amount) public {
        require(userBalances[msg.sender] >= amount, "Insufficient balance");


        userBalances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }


    /// @notice Admin can only withdraw the total accrued fees.
    /// This prevents the rug pull as admin cannot touch user principal.
    /// @param amount The amount of fees the admin wishes to withdraw.
    function adminWithdrawFees(uint256 amount) public {
        require(msg.sender == admin, "Admin control required");
        // Crucial check: Ensure admin only withdraws what's available in fees,
        // as tracked separately from user principal.
        require(totalAccruedFees >= amount, "Insufficient accrued fees for withdrawal");


        totalAccruedFees -= amount; // Deduct the withdrawn amount from available fees
        payable(admin).transfer(amount); // Transfer only the requested fee amount to admin
    }
}


In this improved SecuredVault version, the adminWithdrawFees function no longer directly interacts with user balances. User funds are protected as they are held separately from the totalAccruedFees variable. This alleviates the risk of an admin privilege on the user principal, addressing the core issue seen in the Zunami attack.

Conclusion

Managing administrative privileges is essential for safeguarding security and maintaining user trust. Unrestricted access to assets can result in devastating losses, as demonstrated by incidents like the Zunami Protocol, where unchecked admin privileges led to the complete depletion of user funds. Adhering to this checklist item is critical to preventing such vulnerabilities.

Robust security requires proactive measures, including tightly controlled admin functions and well-designed safeguards to mitigate risks.

In the next article, we will examine another vital aspect of smart contract security: the Sandwich Attack

Stay tuned for further insights on developing secure and resilient decentralized applications!

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.