Skip to content
Granular Pauseability for DeFi protocols

Granular Pauseability for DeFi protocols

As DeFi protocols evolved to manage millions and billions of dollars in user funds, the ability to quickly respond to potential threats or bugs is crucial. Enter the concept of Granular pausing - an approach introduced by Maple Finance V2 that allows the protocol to maintain fine-grained control over the operations.

In this post, we'll look into what granular pausing is, why it's important, and how to implement it in your smart contracts.

What is Granular Pausing?

Granular pausing is an approach that gives flexibility to selectively pause or unpause different parts of the system with varying levels of specificity. Unlike a simple on/off switch for the entire protocol, granular pausing provides three levels of control:

  1. Global Protocol Pause
  2. Contract-Specific Pause
  3. Function-Specific Unpause

Why is it Important?

Imagine a DeFi lending protocol like Maple. One day, they notice unusual activity in one of their lending pools. With a basic pause system, only option would be to shut down the entire protocol - inconveniencing all users and potentially causing panic. Also when some setter function has to be called to fix the bug, they need to unpause the entire protocol, allowing the attack to continue.

With granular pausing, you could:

  1. Pause only the affected lending pool, allowing other pools to operate normally.
  2. Unpause specific function to fix the issue, for ex, update ltvRatio.
  3. If needed, pause the entire protocol quickly while you investigate.
  4. Keep withdrawal functions active even in the paused pool, ensuring users can access their funds.

This flexibility allows for more nuanced risk management, providing a better user experience during incidents.

Implementing Granular Pausing

Let's walk through a basic implementation of a granular pausing system.

Step 1: The GlobalPauseController

First, we'll create a central GlobalPauseController contract that manages the pause states:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
 
import "@openzeppelin/contracts/access/Ownable.sol";
 
contract GlobalPPauseController is Ownable {
    bool public globalPause;
    mapping(address => bool) public contractPause;
    mapping(address => mapping(bytes4 => bool)) public functionUnpause;
 
    event GlobalPauseSet(bool status);
    event ContractPauseSet(address indexed contractAddress, bool status);
    event FunctionUnpauseSet(address indexed contractAddress, bytes4 indexed functionSig, bool status);
 
    function setGlobalPause(bool _status) external onlyOwner {
        globalPause = _status;
        emit GlobalPauseSet(_status);
    }
 
    function setContractPause(address _contract, bool _status) external onlyOwner {
        contractPause[_contract] = _status;
        emit ContractPauseSet(_contract, _status);
    }
 
    function setFunctionUnpause(address _contract, bytes4 _functionSig, bool _status) 
        external 
        onlyOwner 
    {
        functionUnpause[_contract][_functionSig] = _status;
        emit FunctionUnpauseSet(_contract, _functionSig, _status);
    }
 
    /// @dev When the protocol or a contract is paused, we cannot unpause a function, so return `false`
    /// @dev Otherwise check if the given function is unpaused. 
    function isPaused(address _contract, bytes4 _functionSig) 
        external 
        view 
        returns (bool) 
    {
        if (!globalPause && !contractPause[_contract]) {
            return false;
        }
        return !functionUnpause[_contract][_functionSig];
    }
}

This contract allows the owner to set global, contract-specific, and function-specific pause states. The isPaused function determines whether a specific function in a specific contract is currently paused. Instead of using the Ownable contract, one can use the AccessManager contract if more flexibility is required.

Step 2: The Pausable Abstract Contract

Next, we'll create an abstract contract that other contracts can inherit:

abstract contract Pausable {
    GlobalPauseController public gpc;
 
    error Paused();
    constructor(address _gpc) {
        gpc = GlobalPauseController(_gpc);
    }
 
    modifier whenNotPaused() {
        if(gpc.isPaused(address(this), msg.sig)) 
            revert Paused();
        _;
    }
}

This contract provides a whenNotPaused modifier that checks with the GlobalPauseController to determine if the current function is paused.

Step 3: Using the Pausable contract

Finally, let's see how it can be used in a simple lending pool contract:

contract LendingPool is Pausable {
    mapping(address => uint256) public balances;
 
    constructor(address _pauseController) Pausable(_gpc) {}
 
    function deposit() external payable whenNotPaused {
        balances[msg.sender] += msg.value;
    }
 
    function withdraw(uint256 amount) external whenNotPaused {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }
}

In this example, the deposit and withdraw functions use the whenNotPaused modifier, making them subject to the granular pause system.

Conclusion

Granular pausing is a simple yet handy technique for DeFi protocols to balance the need for security with the desire for continuous operations.

Happy coding, and stay secure!


Disclaimer: This blog post is for educational purposes only and does not constitute financial or security advice. Always do your own research and consult with security professionals when implementing critical systems in DeFi protocols.