Give stkWELL holders in the stkWELL/USDC iso market voting rights

Problem: If you stake WELL and supply stkWELL into the iso market, you loose voting rights.

Yes this is the result of some AI (grok) prompting, but there should be a way for stkwell’/USDC iso market to gain voting rights. I know this has been looked at and there is no feasible way to implement, but I would appreciate if any core contributors could at least have a look. Thx

Current Setup and Problem Analysis: Moonwell is a decentralized lending protocol built on Base (and other chains), with WELL as the governance token. Users stake WELL in the Safety Module to receive stkWELL (contract address: 0xe66E3A37C3274Ac24FE8590f7D84A2427194DC17 on Base), which represents their staked position on a 1:1 basis. Staking provides WELL rewards, backstops the protocol against shortfall events, and grants voting power in governance (1 stkWELL = 1 vote). Voting power is also derived from WELL, and delegation is supported to activate and assign votes without transferring ownership. Governance uses a combination of Snapshot (off-chain, based on holding WELL/stkWELL in wallet) and on-chain mechanisms via the Temporal Governor (0x8b621804a7637b781e2BbD58e256a591F2dF7d51 on Base), where total voting power includes delegated tokens and staked amounts.

The stkWELL-USDC isolated market allows users to supply stkWELL (or USDC) as collateral to earn supply APY and potentially borrow the paired asset. Isolated markets confine risks to specific asset pairs, preventing contagion to core markets. When supplying stkWELL, the user transfers stkWELL to the mToken contract (the receipt token for the supplied asset, similar to Compound’s cTokens; I’ll refer to it as mstkWELL for clarity, though the exact address isn’t publicly listed in the docs but can be queried from the market’s comptroller). The user receives mstkWELL, which represents their principal plus accrued interest (calculated via an exchange rate).

The issue: Transferring stkWELL to the mToken contract reduces the user’s stkWELL balance to 0, moving the voting power associated with it to the mToken contract’s balance. Since the mToken contract is not delegated (or delegated to null), those votes are effectively lost—neither the contract votes nor does the user retain them. This discourages users from supplying stkWELL to the market if they want to maintain governance participation.

Proposed Solution: To enable users who supply stkWELL to the isolated market to retain equivalent voting rights, we need to attribute the voting power from the pooled stkWELL proportionally to mstkWELL holders without inflating total governance votes or double-counting. This requires smart contract upgrades to the mToken (to enable past value lookups) and the governor (to include mstkWELL-derived votes in calculations). Since Moonwell is a fork of Compound with open-source contracts (available on GitHub at moonwell-fi/contracts-open-source), these changes are feasible via a Moonwell Improvement Proposal (MIP) submitted with at least 1,000,000 votes on Base.

Step 1: Ensure No Delegation on mstkWELL Contract

  • Confirm or set the mstkWELL contract’s delegate to address(0) on the stkWELL token. This prevents the pooled stkWELL’s voting power from being assigned to any delegate, keeping those votes “dormant” in the system. (If already delegated, a MIP can call stkWELL.delegate(address(0)) from the mstkWELL contract.)

Step 2: Upgrade the mstkWELL Contract for Checkpointing

  • Add checkpointing to track past balances and exchange rates, allowing historical lookups for voting snapshots (required for getPriorVotes compatibility).

  • Key additions:

    • Array for exchange rate checkpoints: struct Checkpoint { uint blockNumber; uint exchangeRate; }

    • Mapping for user balance checkpoints: mapping(address => Checkpoint[]) public balanceCheckpoints;

    • Function to write checkpoints on balance changes (mint, redeem, transfer) and accruals.

    • View functions:

      solidity

      function getPriorBalance(address account, uint blockNumber) public view returns (uint) {
        // Binary search balanceCheckpoints[account] for the last checkpoint <= blockNumber
        // Return the balance at that checkpoint (or 0 if none)
      }
      
      function getPriorExchangeRate(uint blockNumber) public view returns (uint) {
        // Binary search exchangeRateCheckpoints for the last checkpoint <= blockNumber
        // Return the exchangeRate at that checkpoint (default to initial if none)
      }
      
  • This ensures we can compute past underlying stkWELL amounts accurately without replaying accruals (gas-efficient via binary search).

Step 3: Upgrade the Governor Contract to Include mstkWELL Votes

  • Modify the voting power calculation (likely in a function like getVotes or _getVotes) to sum:

    • Standard votes from WELL and stkWELL (via getPriorVotes).

    • Pro-rata votes from mstkWELL: User’s past mstkWELL balance * past exchange rate / 1e18 (mantissa).

  • Example code snippet (assuming a Compound Bravo-style governor; adapt based on exact source):

    solidity

    interface IToken {
        function getPriorVotes(address account, uint blockNumber) external view returns (uint96);
    }
    
    interface IMToken {
        function getPriorBalance(address account, uint blockNumber) external view returns (uint);
        function getPriorExchangeRate(uint blockNumber) external view returns (uint);
    }
    
    contract TemporalGovernor {
        address public well;  // WELL token address
        address public stkWell;  // stkWELL token address
        address public mStkWell;  // mstkWELL token address (set via MIP)
    
        // Existing: Sum votes from WELL and stkWELL
        // New: Add calculated underlying from mstkWELL
        function getVotes(address account, uint blockNumber) public view returns (uint96) {
            uint96 votes = IToken(well).getPriorVotes(account, blockNumber) +
                           IToken(stkWell).getPriorVotes(account, blockNumber);
    
            uint priorBalance = IMToken(mStkWell).getPriorBalance(account, blockNumber);
            uint priorExchangeRate = IMToken(mStkWell).getPriorExchangeRate(blockNumber);
            uint underlying = (priorBalance * priorExchangeRate) / 1e18;  // Adjust for decimals if needed
    
            votes += uint96(underlying);  // Safe cast assuming no overflow (add checks if necessary)
            return votes;
        }
    
        // Add setter for mStkWell if needed, restricted to governance
    }
    
  • This attributes dormant votes from the pool proportionally to mstkWELL holders, maintaining system integrity (no inflation, as pooled votes were previously lost).

Deployment and Activation

  • Deploy upgraded mstkWELL (or migrate liquidity if non-upgradable; Moonwell uses proxy patterns in some contracts).

  • Deploy upgraded governor, transferring authority via MIP.

  • Test on a fork of Base (using tools like Foundry) for gas costs, accuracy, and edge cases (e.g., mid-accrual snapshots).

  • Submit MIP: Discuss on forum.moonwell.fi, then propose on-chain with code/actions (e.g., set new governor, update comptroller if needed).

  • Post-upgrade, users supplying stkWELL will automatically have voting power reflected in governance interfaces (e.g., voting power breakdown includes mstkWELL contribution).

This solution preserves DeFi composability, encourages liquidity in the isolated market, and aligns with Moonwell’s community-driven ethos.

1 Like