Bifrost Developers

stPROS

Professional Liquid Staking for Pharos Network

What is stPROS?

stPROS is Bifrost's liquid staking token for the Pharos Network. By staking PHRS tokens through Bifrost, users receive stPROS tokens that represent their staked position while maintaining liquidity. Staking rewards are automatically reflected in stPROS's increasing exchange rate against PHRS.

Staking rewards

Staking rewards automatically increase the stPROS exchange rate. The longer you hold stPROS, the more PHRS you can redeem when unstaking.

Why stPROS?

Liquidity and Capital Efficiency

stPROS provides liquidity for your staked PHRS, allowing you to trade, transfer, or use it in DeFi protocols while still earning staking rewards.

Automatic Reward Compounding

Staking rewards are automatically compounded into the stPROS exchange rate. No manual claiming required.

No Technical Background Needed

Stake PHRS and receive stPROS with a simple deposit transaction. No need to run validators or manage complex staking infrastructure.

DeFi Composability

stPROS is an ERC-4626 compliant vault token, making it compatible with the broader DeFi ecosystem for lending, liquidity provision, and yield strategies.

Network Information

PropertyValue
NetworkPharos Atlantic Testnet
Chain ID688689
Native TokenPHRS (18 decimals)
RPC Endpointhttps://atlantic.dplabs-internal.com/
Block ExplorerPharosScan

Contract Information

ContractAddress
stPROS0xc9A0B63d91c2A808dD631d031f037944fedDaA12

Contract Architecture

The stPROS contract is part of Bifrost's SLPx V2 contracts, implementing a modular architecture for liquid staking with cross-chain capabilities.

Inheritance Hierarchy

┌─────────────────────────────────────────────────────────────┐
│                         stPROS                              │
│  - depositWithPROS()                                        │
│  - withdrawCompleteToPROS()                                 │
│  - receive() payable                                        │
└─────────────────────────────────────────────────────────────┘

                    extends   │

┌─────────────────────────────────────────────────────────────┐
│               VToken + ReentrancyGuardUpgradeable           │
│  - initialize(asset, owner, name, symbol)                   │
└─────────────────────────────────────────────────────────────┘

                    extends   │

┌─────────────────────────────────────────────────────────────┐
│                       VTokenBase                            │
│  - ERC4626 vault operations                                 │
│  - Async cross-chain minting/redeeming                      │
│  - Withdrawal queue system                                  │
│  - Oracle-based pricing                                     │
│  - Pausable + Access Control                                │
└─────────────────────────────────────────────────────────────┘

System Components

The stPROS system consists of several interconnected contracts:

ContractPurpose
stPROSMain entry point for PHRS staking, handles native token wrapping
VTokenInitialization wrapper extending VTokenBase
VTokenBaseCore ERC4626 vault logic with async cross-chain operations
OracleManages exchange rates between PHRS and stPROS
BridgeVaultReceives bridged assets from other chains
TokenGatewayHandles cross-chain token teleportation

stPROS Contract

The stPROS contract extends VToken and adds functionality specific to handling PHRS (the native token of Pharos).

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

import {VToken} from "./VToken.sol";
import {IWETH} from "./interfaces/IWETH.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";

contract stPROS is VToken, ReentrancyGuardUpgradeable {
    // Custom errors
    error EthNotSent();
    error EthTransferFailed();

    // Events
    event EthReceived(address indexed sender, uint256 amount);

    // Accept native PHRS
    receive() external payable {
        emit EthReceived(msg.sender, msg.value);
    }

    // Deposit native PHRS to mint stPROS
    function depositWithPROS(uint256 amount) external payable nonReentrant whenNotPaused {
        if (msg.value == 0) revert EthNotSent();
        // Wraps PHRS to V_PHRS, then mints stPROS shares
        // ...
    }

    // Withdraw and receive native PHRS
    function withdrawCompleteToPROS(uint256 withdrawId) external nonReentrant whenNotPaused {
        // Burns stPROS, unwraps V_PHRS, sends native PHRS to caller
        // ...
    }
}

stPROS-Specific Functions

FunctionVisibilityDescription
depositWithPROS(uint256 amount)external payableAccepts native PHRS, wraps to V_PHRS, mints stPROS shares
withdrawCompleteToPROS(uint256 withdrawId)externalCompletes withdrawal, unwraps V_PHRS, returns native PHRS
receive()external payableFallback to accept native PHRS transfers

VTokenBase (Core Vault Logic)

VTokenBase implements the ERC4626 vault standard with extensions for cross-chain operations.

State Variables

// Oracle for exchange rate calculations
IOracle public oracle;

// Bridge vault for cross-chain asset custody
address public bridgeVault;

// Token gateway for cross-chain teleportation
ITokenGateway public tokenGateway;

// Authorized address for triggering bridge operations
address public triggerAddress;

// Cycle-based tracking for batched operations
uint256 public currentCycle;
mapping(uint256 => CycleInfo) public cycleInfo;

// Per-user withdrawal queue
mapping(address => WithdrawalQueue) public withdrawalQueues;

Core ERC4626 Functions

FunctionDescription
deposit(uint256 assets, address receiver)Deposit assets, receive shares
mint(uint256 shares, address receiver)Mint exact shares by depositing assets
withdraw(uint256 assets, address receiver, address owner)Withdraw assets by burning shares
redeem(uint256 shares, address receiver, address owner)Redeem shares for assets
convertToShares(uint256 assets)Preview assets → shares conversion
convertToAssets(uint256 shares)Preview shares → assets conversion
previewDeposit(uint256 assets)Preview deposit result
previewRedeem(uint256 shares)Preview redeem result
totalAssets()Total assets managed by vault

Async Cross-Chain Functions

These functions enable minting and redeeming across chains via the Bifrost bridge:

FunctionDescription
asyncMint(uint256 shares, address receiver)Queue mint request for cross-chain execution
asyncMintWithNativeCost(uint256 shares, address receiver)Async mint paying relayer fees in native token
asyncRedeem(uint256 shares, address receiver)Queue redeem request for cross-chain execution
asyncRedeemWithNativeCost(uint256 shares, address receiver)Async redeem paying relayer fees in native token

Withdrawal Queue Functions

FunctionDescription
getWithdrawalQueue(address user)Get user's pending withdrawals
withdrawComplete(uint256 withdrawId)Complete a pending withdrawal
getWithdrawableAmount(address user)Get total amount available to withdraw

Admin Functions

FunctionDescription
setOracle(address _oracle)Set the oracle contract address
setBridgeVault(address _bridgeVault)Set the bridge vault address
setTokenGateway(address _tokenGateway)Set the token gateway address
setTriggerAddress(address _triggerAddress)Set authorized trigger address
pause()Pause user-facing operations
unpause()Resume operations

Oracle Contract

The Oracle manages exchange rates between underlying assets and vTokens.

contract Oracle {
    struct PoolInfo {
        uint256 tokenAmount;   // Total underlying tokens
        uint256 vTokenAmount;  // Total vToken supply
    }

    uint256 public constant FEE_DENOMINATOR = 10000;
    uint256 public mintFeeRate;   // Fee on minting (basis points)
    uint256 public redeemFeeRate; // Fee on redeeming (basis points)

    mapping(address => PoolInfo) public poolInfo;
}

Oracle Functions

FunctionDescription
getVTokenAmountByToken(address token, uint256 amount)Calculate vToken output for token input (with mint fee)
getTokenAmountByVToken(address token, uint256 amount)Calculate token output for vToken input (with redeem fee)
setPoolInfo(address token, uint256 tokenAmount, uint256 vTokenAmount)Update pool exchange rate
setMintFeeRate(uint256 rate)Set mint fee (max 10000 = 100%)
setRedeemFeeRate(uint256 rate)Set redeem fee (max 10000 = 100%)

BridgeVault Contract

The BridgeVault receives and custodies assets bridged from other chains.

contract BridgeVault {
    // Whitelist of authorized VToken contracts
    mapping(address => bool) public vTokens;

    // Only whitelisted VTokens can withdraw
    modifier onlyVToken() {
        require(vTokens[msg.sender], "Not authorized");
        _;
    }
}

BridgeVault Functions

FunctionDescription
withdraw(address token, address to, uint256 amount)Withdraw tokens (VToken only)
withdrawETH(address to, uint256 amount)Withdraw native currency (VToken only)
addVToken(address vToken)Whitelist a VToken contract
removeVToken(address vToken)Remove VToken from whitelist
emergencyWithdraw(address token, uint256 amount)Owner emergency withdrawal

Security Features

The stPROS contract implements multiple security measures:

  1. Reentrancy Protection: All state-changing functions use nonReentrant modifier
  2. Pausable: Operations can be paused in emergencies via whenNotPaused
  3. Access Control: Role-based permissions for admin functions
  4. Safe Transfers: Uses OpenZeppelin's SafeERC20 for token transfers
  5. Input Validation: Custom errors for invalid inputs (EthNotSent, EthTransferFailed)

Events

// stPROS Events
event EthReceived(address indexed sender, uint256 amount);

// VTokenBase Events (inherited)
event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares);
event AsyncMintQueued(address indexed receiver, uint256 shares, uint256 cycle);
event AsyncMintCompleted(address indexed receiver, uint256 shares);
event AsyncRedeemQueued(address indexed owner, uint256 shares, uint256 cycle);
event AsyncRedeemCompleted(address indexed owner, uint256 assets);
event BridgeVaultChanged(address indexed oldVault, address indexed newVault);
event OracleChanged(address indexed oldOracle, address indexed newOracle);

Source Code

The complete contract source code is available at:

Integration Guide

Prerequisites

To integrate stPROS, you'll need:

  • A wallet connected to Pharos Atlantic Testnet
  • PHRS tokens for staking
  • wagmi or viem for contract interactions

ABI

The stPROS contract implements the ERC-4626 vault standard with additional async operations for cross-chain functionality.

export const stProsAbi = [
  // Core ERC-4626 Functions
  {
    name: "deposit",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "assets", type: "uint256" },
      { name: "receiver", type: "address" }
    ],
    outputs: [{ name: "shares", type: "uint256" }]
  },
  {
    name: "depositWithPROS",
    type: "function",
    stateMutability: "payable",
    inputs: [{ name: "amount", type: "uint256" }],
    outputs: []
  },
  {
    name: "withdraw",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "assets", type: "uint256" },
      { name: "receiver", type: "address" },
      { name: "owner", type: "address" }
    ],
    outputs: [{ name: "shares", type: "uint256" }]
  },
  {
    name: "redeem",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "shares", type: "uint256" },
      { name: "receiver", type: "address" },
      { name: "owner", type: "address" }
    ],
    outputs: [{ name: "assets", type: "uint256" }]
  },
  // Preview Functions
  {
    name: "previewDeposit",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "assets", type: "uint256" }],
    outputs: [{ name: "shares", type: "uint256" }]
  },
  {
    name: "previewRedeem",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "shares", type: "uint256" }],
    outputs: [{ name: "assets", type: "uint256" }]
  },
  {
    name: "convertToShares",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "assets", type: "uint256" }],
    outputs: [{ name: "shares", type: "uint256" }]
  },
  {
    name: "convertToAssets",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "shares", type: "uint256" }],
    outputs: [{ name: "assets", type: "uint256" }]
  },
  // ERC-20 Functions
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ name: "", type: "uint256" }]
  },
  {
    name: "totalSupply",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }]
  },
  {
    name: "totalAssets",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }]
  },
  {
    name: "asset",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "address" }]
  },
  // Async Operations (Cross-chain)
  {
    name: "asyncMint",
    type: "function",
    stateMutability: "payable",
    inputs: [
      { name: "shares", type: "uint256" },
      { name: "receiver", type: "address" }
    ],
    outputs: []
  },
  {
    name: "asyncRedeem",
    type: "function",
    stateMutability: "payable",
    inputs: [
      { name: "shares", type: "uint256" },
      { name: "receiver", type: "address" }
    ],
    outputs: []
  },
  // Events
  {
    name: "Deposit",
    type: "event",
    inputs: [
      { name: "sender", type: "address", indexed: true },
      { name: "owner", type: "address", indexed: true },
      { name: "assets", type: "uint256", indexed: false },
      { name: "shares", type: "uint256", indexed: false }
    ]
  },
  {
    name: "Withdraw",
    type: "event",
    inputs: [
      { name: "sender", type: "address", indexed: true },
      { name: "receiver", type: "address", indexed: true },
      { name: "owner", type: "address", indexed: true },
      { name: "assets", type: "uint256", indexed: false },
      { name: "shares", type: "uint256", indexed: false }
    ]
  }
] as const;

Minting stPROS (Staking PHRS)

Use the depositWithPROS function to stake PHRS and receive stPROS:

import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { parseEther } from 'viem';

const STPROS_ADDRESS = '0xc9A0B63d91c2A808dD631d031f037944fedDaA12';

function MintStPROS() {
  const { writeContract, data: hash } = useWriteContract();

  const { isLoading, isSuccess } = useWaitForTransactionReceipt({
    hash,
  });

  const handleMint = (amount: string) => {
    writeContract({
      address: STPROS_ADDRESS,
      abi: stProsAbi,
      functionName: 'depositWithPROS',
      args: [parseEther(amount)],
      value: parseEther(amount), // Send PHRS as native value
    });
  };

  return (
    <button onClick={() => handleMint('10')} disabled={isLoading}>
      {isLoading ? 'Minting...' : 'Mint stPROS'}
    </button>
  );
}

Reading stPROS Balance

import { useReadContract } from 'wagmi';
import { formatEther } from 'viem';

function StPROSBalance({ address }: { address: `0x${string}` }) {
  const { data: balance } = useReadContract({
    address: STPROS_ADDRESS,
    abi: stProsAbi,
    functionName: 'balanceOf',
    args: [address],
  });

  return <p>stPROS Balance: {balance ? formatEther(balance) : '0'}</p>;
}

Getting the Exchange Rate

Use convertToShares to preview how much stPROS you'll receive for a given amount of PHRS:

import { useReadContract } from 'wagmi';
import { parseEther, formatEther } from 'viem';

function ExchangeRate() {
  const { data: shares } = useReadContract({
    address: STPROS_ADDRESS,
    abi: stProsAbi,
    functionName: 'convertToShares',
    args: [parseEther('1')], // 1 PHRS
  });

  return (
    <p>
      Exchange Rate: 1 PHRS = {shares ? formatEther(shares) : '...'} stPROS
    </p>
  );
}

Redeeming stPROS (Unstaking)

Use the redeem function to convert stPROS back to PHRS:

import { useWriteContract } from 'wagmi';
import { parseEther } from 'viem';

function RedeemStPROS({ userAddress }: { userAddress: `0x${string}` }) {
  const { writeContract } = useWriteContract();

  const handleRedeem = (shares: string) => {
    writeContract({
      address: STPROS_ADDRESS,
      abi: stProsAbi,
      functionName: 'redeem',
      args: [
        parseEther(shares),  // shares to redeem
        userAddress,          // receiver
        userAddress,          // owner
      ],
    });
  };

  return (
    <button onClick={() => handleRedeem('10')}>
      Redeem 10 stPROS
    </button>
  );
}

Complete Integration Example

import {
  useAccount,
  useBalance,
  useReadContract,
  useWriteContract,
  useWaitForTransactionReceipt
} from 'wagmi';
import { parseEther, formatEther } from 'viem';
import { useState } from 'react';

const STPROS_ADDRESS = '0xc9A0B63d91c2A808dD631d031f037944fedDaA12';

export function StPROSStaking() {
  const [amount, setAmount] = useState('');
  const { address } = useAccount();

  // Get native PHRS balance
  const { data: phrsBalance } = useBalance({ address });

  // Get stPROS balance
  const { data: stProsBalance } = useReadContract({
    address: STPROS_ADDRESS,
    abi: stProsAbi,
    functionName: 'balanceOf',
    args: [address!],
    query: { enabled: !!address },
  });

  // Get exchange rate
  const { data: exchangeRate } = useReadContract({
    address: STPROS_ADDRESS,
    abi: stProsAbi,
    functionName: 'convertToShares',
    args: [parseEther('1')],
  });

  // Mint transaction
  const { writeContract, data: hash } = useWriteContract();
  const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });

  const handleMint = () => {
    if (!amount) return;
    writeContract({
      address: STPROS_ADDRESS,
      abi: stProsAbi,
      functionName: 'depositWithPROS',
      args: [parseEther(amount)],
      value: parseEther(amount),
    });
  };

  return (
    <div>
      <h2>stPROS Staking</h2>

      <div>
        <p>PHRS Balance: {phrsBalance ? formatEther(phrsBalance.value) : '0'}</p>
        <p>stPROS Balance: {stProsBalance ? formatEther(stProsBalance) : '0'}</p>
        <p>Rate: 1 PHRS = {exchangeRate ? formatEther(exchangeRate) : '...'} stPROS</p>
      </div>

      <input
        type="number"
        value={amount}
        onChange={(e) => setAmount(e.target.value)}
        placeholder="Amount of PHRS to stake"
      />

      <button onClick={handleMint} disabled={isLoading || !amount}>
        {isLoading ? 'Minting...' : 'Stake PHRS'}
      </button>

      {isSuccess && <p>Successfully minted stPROS!</p>}
    </div>
  );
}

Key Functions Reference

FunctionDescriptionParameters
depositWithPROSStake PHRS to receive stPROSamount: uint256 (wei)
depositERC-4626 depositassets: uint256, receiver: address
withdrawWithdraw PHRS from stPROSassets: uint256, receiver: address, owner: address
redeemRedeem stPROS shares for PHRSshares: uint256, receiver: address, owner: address
convertToSharesPreview PHRS → stPROS conversionassets: uint256
convertToAssetsPreview stPROS → PHRS conversionshares: uint256
previewDepositPreview deposit resultassets: uint256
previewRedeemPreview redeem resultshares: uint256
balanceOfGet stPROS balanceaccount: address
totalAssetsGet total PHRS in vault-
totalSupplyGet total stPROS supply-

FAQ

What is the minimum stake?

There is no enforced minimum, but gas costs should be considered for small amounts.

How do staking rewards work?

Staking rewards are automatically reflected in the exchange rate between PHRS and stPROS. As rewards accumulate, each stPROS becomes redeemable for more PHRS.

Is stPROS transferable?

Yes, stPROS is an ERC-20 compatible token that can be freely transferred between addresses.

What is the stPROS contract standard?

stPROS implements the ERC-4626 tokenized vault standard, ensuring compatibility with DeFi protocols that support this standard.

Resources

On this page