← Back to list

Trust, Risk, and the Flying Tulip PUT

A Deep Dive into Centralization Risks and Design Trade-offs

Tanu Gupta · 2026-01-19 14:04 · 1 claps · 6.7 min read
#security-review #governance-risk #smart-contract-auditing #web3 #flying-tulip
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3

Security Review of the Flying Tulip PUT — A Deep Dive into Centralization Risks and Design Trade-offs

Recently, I had the opportunity to audit the Flying Tulip PUT protocol, a cash-secured put options platform where users deposit collateral in exchange for PUT NFTs. The protocol’s core promise is principal protection with yield generation — all collateral is deployed to yield strategies while principal remains protected.

This blog post shares my journey through the codebase, the issues I found, and the broader lessons about centralization in DeFi protocols.

The Protocol at a Glance

Flying Tulip PUT consists of four main components:

  1. PutManager: Orchestrates the entire protocol lifecycle — public offering, investments, collateral registry, and exit mechanisms

  2. pFT: ERC721 NFTs representing PUT positions

  3. ftYieldWrapper: ERC20 wrapper managing collateral deployment to yield strategies

  4. Strategy Adapters: Modular contracts interfacing with external DeFi protocols (Aave, StETH, etc.)

The protocol operates in two phases:

  • Public Offering: Users invest collateral, receive PUT NFTs, and get allocated FT tokens
  • Post-Offering: Users can withdraw FT tokens, divest for underlying collateral, or receive position tokens in-kind

The Centralization Elephant in the Room

One of the first things that struck me during the audit was the extensive reliance on trusted roles. The protocol has multiple privileged actors:

  • msig: Can pause/unpause, upgrade contracts, set oracles, list collateral, withdraw divested capital
  • configurator: Controls sale flags, FT liquidity, collateral caps
  • yieldClaimer/strategyManager: Manages strategy deployment and yield claiming
  • Circuit Breaker Owner: Can override rate limits

This isn’t necessarily wrong — many DeFi protocols rely on governance. But it creates a fundamental question: Where do we draw the line between operational necessity and centralization risk?

The Price Manipulation Powers

Perhaps the most concerning aspect of centralization is the direct control over pricing mechanisms. The msig has several ways to influence how investments are priced:

  1. Setting ftPerUSD Without Bounds

The FlyingTulipOracle contract allows msig to set the ftPerUSD rate — the conversion rate between FT tokens and USD — with no bounds validation:

function setftPerUSD(uint64 newFtPerUSD) external onlyMsig {
    if (newFtPerUSD == 0) revert ftOracleError();
    // No maximum bound check!
    ftPerUSD = newFtPerUSD;
    emit ftPerUSDUpdated(newFtPerUSD);
}

The Impact:

  • msig can set ftPerUSD to 1 (1 FT = $0.00000001) : Users get almost no FT for their collateral
  • msig can set ftPerUSD to type(uint64).max : Users get massive amounts of FT (potential overflow risk)
  • This directly affects the economic value of every investment
  • No timelock, no community review, no bounds checking
  1. Controlling Price Bounds

The oracle uses Aave’s oracle as the base price source, but msig can set arbitrary price bounds that can effectively disable price validation:

// FlyingTulipOracle.sol
function setPriceBounds(address token, uint256 minP, uint256 maxP) external onlyMsig {
    // allow 0 to mean "unset"
    if (minP != 0 && maxP != 0 && minP > maxP) {
        revert ftOracleError();
    }
    minPrice[token] = minP;
    maxPrice[token] = maxP;
    emit PriceBoundsUpdated(token, minP, maxP);
}

// getAssetPrice() uses these bounds:
function getAssetPrice(address token) public view returns (uint256 strike) {
    strike = aaveOracle.getAssetPrice(token);
    if (strike == 0) revert ftOracleError();
    uint256 minP = minPrice[token];
    uint256 maxP = maxPrice[token];
    // If minP = 0 or maxP = 0, bounds are effectively disabled!
    if ((minP != 0 && strike < minP) || (maxP != 0 && strike > maxP)) {
        revert ftOracleError();
    }
}

The Problem:

  • Setting minP = 0 and maxP = 0 disables all price bounds
  • msig can set bounds to extreme values (e.g., minP = 1, maxP = type(uint256).max) : effectively no protection
  • If Aave oracle is manipulated or fails, the bounds don’t help if they’re set to 0
  • No validation that bounds are reasonable
  1. Changing the Oracle Entirely

Beyond setting parameters, msig can replace the entire oracle contract:

// PutManager.sol
function setOracle(address _oracle) external onlyMsig {
    if (_oracle == address(0)) revert ftPutManagerZeroAddress();
    ftOracle = IFlyingTulipOracle(_oracle);
    emit OracleUpdated(_oracle);
}

The Risk:

  • msig can deploy a malicious oracle that returns arbitrary prices
  • No validation that the new oracle is legitimate
  • No timelock or community approval
  • Can be done in a single transaction

The Fund Movement Powers

Beyond pricing, msig and other privileged roles have significant control over fund movement:

  1. Claiming All Yield to Treasury

The yieldClaimer role can claim yield from strategies and send it directly to the treasury:

// ftYieldWrapper.sol
function claimYield(address _strategy) external onlyYieldClaimers returns (uint256 _yield) {
    if (!isStrategy(_strategy)) revert ftYieldWrapperNotStrategy();
    _yield = IStrategy(_strategy).claimYield(treasury);  // Direct to treasury
    if (_yield == 0) revert ftYieldWrapperNoYield();
    emit YieldClaimed(msg.sender, address(token), _yield);
}

// Or claim from ALL strategies at once:
function claimYields() external onlyYieldClaimers returns (uint256 _yield) {
    uint256 strategiesLength = strategies.length;
    address _treasury = treasury;
    for (uint256 i = 0; i < strategiesLength; i++) {
        _yield += IStrategy(strategies[i]).claimYield(_treasury);
    }
    // ...
}

The Power:

  • yieldClaimer can drain all yield from all strategies in a single transaction
  • No rate limiting or gradual claiming
  • Treasury address is controlled by governance
  • If treasury is compromised, all yield is at risk
  1. Sweeping Idle Yield

Beyond strategy yield, yieldClaimer can sweep idle yield (surplus tokens sitting in the wrapper):

//ftYieldWrapper.sol
function sweepIdleYield() external nonReentrant onlyYieldClaimers returns (uint256 amount) {
    uint256 idleBalance = IERC20(token).balanceOf(address(this));
    uint256 liabilities = totalSupply();
    if (idleBalance <= liabilities) revert ftYieldWrapperNoYield();
    amount = idleBalance - liabilities;
    IERC20(token).safeTransfer(treasury, amount);  // Direct transfer
    emit YieldSwept(msg.sender, address(token), amount);
}

The Risk:

  • Any surplus tokens (beyond what’s needed for principal) can be swept
  • No validation of what constitutes “idle” vs. “needed for operations
  • Direct transfer to treasury (no intermediate checks)
  1. Deploying Capital to Strategies

The yieldClaimer can deploy user capital to strategies:

// ftYieldWrapper.sol
function deploy(address strategy, uint256 amount) external nonReentrant onlyYieldClaimer {
    if (!isStrategy(strategy)) revert ftYieldWrapperNotStrategy();
    if (amount == 0) revert ftYieldWrapperInsufficientLiquidity();

    uint256 balance = IERC20(token).balanceOf(address(this));
    if (balance < amount) revert ftYieldWrapperInsufficientLiquidity();

    // Deploy to strategy
    IERC20(token).safeApprove(strategy, amount);
    uint256 shares = IStrategy(strategy).deposit(amount);

    deployedToStrategy[strategy] += shares;
    deployed += shares;
    emit Deployed(strategy, shares);
}

The Control:

  • yieldClaimer decides which strategies get capital
  • yieldClaimer decides how much goes to each strategy
  • No automatic rebalancing or strategy selection
  • If a strategy is malicious or gets compromised, user capital is at risk
  1. Strategy Execution (God Mode)

Perhaps the most powerful function is execute(), which allows yieldClaimer to call arbitrary functions on strategies:

// ftYieldWrapper.sol
function execute(
    address _strategy,
    address to,
    uint256 value,
    bytes calldata data
)
    external
    onlyYieldClaimers
    returns (bool success, bytes memory result)
{
    if (!isStrategy(_strategy)) revert ftYieldWrapperNotStrategy();
    return IStrategy(_strategy).execute(to, value, data);  // Arbitrary call!
}

The Power:

  • yieldClaimer can call any function on any strategy
  • Can send ETH (value parameter)
  • Can pass arbitrary data (function calls with any parameters)
  • Only protection: strategies should enforce valueOfCapital() >= totalSupply() after execution
  • But if a strategy has a bug, this could be bypassed

The Operational Control Powers

Beyond pricing and fund movement, privileged roles control critical operational parameters:

  1. Listing New Collateral

Only msig can add new collateral types:

// PutManager.sol
function addAcceptedCollateral(address _collateral, address _vault) external onlyMsig {
    // Validates oracle price, decimals, vault token match
    // But msig controls which tokens are accepted
}

The Risk:

  • msig could list a malicious token or vault
  • If vault is compromised, all deposits to that collateral type are at risk
  • No community approval or timelock
  1. Controlling Sale State

The configurator has significant control over the public offering:

// PutManager.sol
function setSaleEnabled(bool _saleEnabled) external onlyConfigurator {
    _setSaleEnabled(_saleEnabled);
}

// PutManager.sol
function addFTLiquidity(uint256 amount) external onlyConfigurator {
    FT.safeTransferFrom(msg.sender, address(this), amount);
    ftOfferingSupply += amount;  // Controls how much FT is available
}

The Control:

  • configurator can pause the sale at any time
  • configurator controls how much FT is available for allocation
  • configurator can set collateral caps per token
  • No checks that caps are fair or reasonable
  1. Upgrading Contracts

Both PutManager and pFT are upgradeable via UUPS proxy:

// PutManager.sol
function _authorizeUpgrade(address newImplementation) internal override onlyMsig {}

The Ultimate Power:

  • msig can upgrade the entire contract logic
  • Can change any function behavior
  • Can add new functions, remove checks, change access controls
  • No timelock (though msig rotation has a 1-hour delay)

The Centralization Spectrum in Practice

When I mapped out all these powers, I realized Flying Tulip PUT sits in a highly centralized position on the spectrum:

What Admins Can Do:

  1. ✅ Manipulate pricing (ftPerUSD, price bounds, oracle replacement)

  2. ✅ Move funds (withdraw divested capital, claim yield, sweep idle)

  3. ✅ Control operations (pause sale, set caps, list collateral)

  4. ✅ Deploy capital (choose strategies, allocate amounts)

  5. ✅ Execute arbitrary code (via execute() on strategies)

  6. ✅ Upgrade contracts (change any logic)

What Protects Users:

  1. ⚠️ Multi-sig governance (requires multiple signatures)

  2. ⚠️ 1-hour delay on msig rotation (but not on other actions)

  3. ⚠️ Strategy-level checks (valueOfCapital() >= totalSupply())

  4. ⚠️ Community monitoring (users can see on-chain actions)

What’s Missing:

  1. ❌ Timelocks on critical parameter changes

  2. ❌ Bounds validation on pricing parameters

  3. ❌ Rate limiting on fund withdrawals

  4. ❌ Community approval for major changes

  5. ❌ Automatic safeguards (beyond strategy-level checks)

The Trust Model

The protocol’s trust model is essentially:

Trust the multi-sig to act in users’ best interests, with minimal technical safeguards.

This isn’t necessarily wrong — many successful protocols use this model. But it requires:

  • Transparency: All actions are on-chain and visible
  • Reputation: The team’s reputation is on the line
  • Community: Users can monitor and respond to bad actions
  • Legal: Potential legal recourse if funds are misused

However, from a “technical security” perspective, the protocol relies heavily on trust rather than cryptographic guarantees.

Conclusion

Auditing Flying Tulip PUT was a fascinating journey through the complexities of DeFi security. The protocol is well-designed overall, but it highlights the inherent tension between security and operational flexibility.

The Bigger Picture:

This audit reinforced my belief that transparency and documentation are as important as technical security. Users need to understand:

  • What they’re trusting
  • Who controls what
  • What the risks are
  • How to monitor the protocol

Final Thoughts:

DeFi protocols will always have some degree of centralization. The question isn’t “is it centralized?” but rather:

  • Is the centralization acceptable?
  • Is it clearly disclosed?
  • Are there safeguards in place?
  • Can the community respond if needed?

But overall, it’s a protocol that makes reasonable trade-offs between security and flexibility.

The key is ensuring users understand those trade-offs.

What are your thoughts on centralization in DeFi? Have you encountered similar dilemmas in your audits? I’d love to hear your perspective in the comments below.


메타데이터
post_id
c59338dccd94
slug
trust-risk-and-the-flying-tulip-put-c59338dccd94
url
https://medium.com/@tanu_G/trust-risk-and-the-flying-tulip-put-c59338dccd94
canonical_url
https://medium.com/@tanu_G/trust-risk-and-the-flying-tulip-put-c59338dccd94
author_url
https://medium.com/@tanu_G
status
ok
fetched_at
2026-06-12 07:40:50