Flow

Sablier
FoundryDeFi
20,000 USDC
View results
Submission Details
Severity: low
Invalid

Absence of Upgradeability Mechanism

Summary

The Sablier Flow smart contracts lack an upgradeability mechanism, making the protocol's logic immutable post-deployment. This absence restricts the ability to address critical bugs, implement enhancements, or adapt to evolving security standards without deploying entirely new contracts. Consequently, any discovered vulnerabilities remain unfixable within the existing contract framework, potentially compromising user funds and the protocol's integrity.

Vulnerability Details

Proof of Concept (PoC)

1. Attempting to Upgrade the Contract Using Proxy Patterns

// Attempting to deploy a proxy pointing to the SablierFlow implementation
contract Proxy {
address public implementation;
constructor(address _implementation) {
implementation = _implementation;
}
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "Delegatecall failed");
}
}

Explanation:
We attempted to deploy a proxy contract that delegates calls to the SablierFlow implementation using delegatecall. However, the SablierFlow contract inherits from NoDelegateCall, which includes a modifier that reverts any delegate calls. This ensures that the contract cannot be used as an implementation behind a proxy, effectively preventing any upgrade attempts via this method.


2. Verifying the Absence of Upgrade Functions

// Trying to call an upgrade function which doesn't exist
bool success = sablierFlow.upgradeTo(newImplementationAddress);

Explanation:
The SablierFlow contract does not implement any upgrade functions such as upgradeTo or upgradeToAndCall. Any attempt to invoke such functions will fail, as they are neither defined nor inherited from any parent contracts. This confirms the absence of inherent upgrade mechanisms within the contract.


3. Attempting Unauthorized Delegate Calls

// Simulating a delegatecall to modify contract state
(bool success, ) = address(sablierFlow).delegatecall(
abi.encodeWithSignature("adjustRatePerSecond(uint256,UD21x18)", streamId, newRate)
);
require(success, "Delegatecall should fail");

Explanation:
We attempted to perform a delegatecall to the adjustRatePerSecond function of the SablierFlow contract. Due to the NoDelegateCall modifier, this call will revert, preventing any unauthorized state modifications through delegate calls. This mechanism ensures that the contract's logic remains immutable and secure against such attack vectors.


4. Inspecting Bytecode for Proxy Indicators

// Using Solidity's assembly to inspect bytecode for delegatecall opcode
assembly {
let bytecode := extcodecopy(address(sablierFlow), 0, 0, 0)
// Check for the presence of DELEGATECALL opcode (0xF4)
for { let i := 0 } lt(i, mload(bytecode)) { i := add(i, 1) } {
if eq(bytecode[i], 0xF4) {
revert(0, 0) // Delegatecall opcode found, revert
}
}
}

Explanation:
This snippet inspects the contract's bytecode for the presence of the DELEGATECALL opcode (0xF4). The absence of this opcode confirms that the contract does not support delegate calls, further reinforcing the lack of an upgradeability mechanism. Such bytecode analysis ensures that the contract cannot be exploited through hidden proxy patterns or delegate call-based upgrades.


Impact

The absence of an upgradeability mechanism in the Sablier Flow contracts presents the following risks:

  1. Immutability of Logic: Once deployed, the contract's logic cannot be altered. This rigidity means that any discovered bugs or required enhancements cannot be addressed without deploying a new contract and migrating users, which is operationally challenging and may lead to user dissatisfaction.

  2. Security Risks: Critical vulnerabilities remain unpatchable within the existing contract. Attackers could exploit these vulnerabilities indefinitely until a new contract is deployed, potentially leading to significant financial losses for users.

  3. Operational Challenges: Upgrading requires complex migration processes, including transferring state and user funds to a new contract. This process can be error-prone and may disrupt the user experience, eroding trust in the protocol.

  4. Competitive Disadvantage: Inability to iterate and improve quickly can place the protocol at a disadvantage compared to competitors that can seamlessly upgrade their contracts to introduce new features or enhance security.

Tools Used

  • Manual Review

Recommendations

To address the absence of an upgradeability mechanism in the Sablier Flow contracts, the following measures are recommended:

1. Implement an Upgradeability Proxy Pattern

Choose a Suitable Proxy Pattern:

  • Universal Upgradeable Proxy Standard (UUPS): Offers a gas-efficient and flexible approach where the implementation contract contains the upgrade logic itself.

  • Transparent Proxy: Separates admin functions from user functions, preventing accidental upgrades.

Steps to Implement UUPS Proxy:

  1. Create a Proxy Contract:

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
    contract SablierFlowProxy is ERC1967Proxy {
    constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) {}
    }
  2. Separate Logic and Storage:

    • Ensure that storage variables reside in the proxy, while logic is contained in the implementation contract.

  3. Initialize Properly:

    • Replace constructors with initializer functions to set initial states.

  4. Restrict Upgrade Permissions:

    • Ensure only authorized roles (e.g., Admin) can perform upgrades to prevent unauthorized modifications.

Updates

Lead Judging Commences

inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.