Skip to main content

How to Integrate SuperBoring?

This guide explains how to integrate SuperBoring into your dApp or smart contract using the SBMacro contract. SBMacro provides a convenient API for easy onboarding to SuperBoring, allowing you to create a DCA (Dollar-Cost Averaging) flow to a SuperBoring Torex with customizable parameters.

In the example below, we demonstrate how to integrate SuperBoring into a React client and a Solidity smart contract. The example shows how to start a SuperBoring DCA flow by calling the SBMacro contract with the required parameters.

Will this work for starting, updating and stopping DCA flows?

The SBMacro contract can be used to start, update DCA flows.

In order to stop a DCA flow, you should use the CFAv1Forwarder contract available on all networks, and call the method deteleFlow with the correct parameters.

SBMacro Deployment Addresses

The SBMacro contract is deployed on the following networks:

  • Optimism Sepolia: 0x34Db26737185671215fB90E2F8C6fd8C4F8eB944
  • Celo Mainnet: 0xC25a0B78C1d9F403F33834639FfF5798847015F8
  • Base Mainnet: 0xE581E09a9c2a9188c3E6F0fAb5a0b3EC88cA39aE
  • Optimism Mainnet: 0x383329703f346d72F4b86111a502daaa8f2c69C7

The MacroForwarder address is the same for all networks:

  • All Superfluid-supported networks: 0xfD01285b9435bc45C243E5e7F978E288B2912de6
What is the MacroForwarder?

The MacroForwarder is one of the trusted forwarder contracts that allows you to call the SBMacro contract with encoded parameters. Th MacroForwarder is useful for batching operations and guaranteeing a good user experience.

React Client Integration

The example below demonstrates how to integrate SuperBoring into a React client using the SBMacro contract. It shows the creation of a form that allows users to start a SuperBoring DCA flow by entering the required parameters.

About ethers.js

This example uses ethers.js to interact with the smart contract. Make sure to install it by running npm install ethers.

You can choose to use another library if you prefer (eg. viem, wagmi, web3js etc), but the example code will need to be adjusted accordingly.

Example JSX Component Code

Live Editor
// Don't forget to install ethers.js: "npm install ethers" and make the right imports
//import React, { useState } from 'react';
//import { ethers } from 'ethers';

function SuperBoringDCAForm() {
  const [torexAddr, setTorexAddr] = React.useState('');
  const [flowRate, setFlowRate] = React.useState('');
  const [distributor, setDistributor] = React.useState('');
  const [referrer, setReferrer] = React.useState('');
  const [upgradeAmount, setUpgradeAmount] = React.useState('');
  const [status, setStatus] = React.useState('');
  const [isConnected, setIsConnected] = React.useState(false);
  const [walletAddress, setWalletAddress] = React.useState('');
  const [chainId, setChainId] = React.useState(null);
  const [maxBalance, setMaxBalance] = React.useState(null);
  const [allowance, setAllowance] = React.useState(null);
  const [isTorexValid, setIsTorexValid] = React.useState(false);
  const [inTokenAddress, setInTokenAddress] = React.useState(null);
  const [underlyingTokenAddress, setUnderlyingTokenAddress] = React.useState(null);

  React.useEffect(() => {
    checkIfWalletIsConnected();
    if (window.ethereum) {
      window.ethereum.on('chainChanged', handleChainChanged);
      window.ethereum.on('accountsChanged', handleAccountsChanged);
    }
    return () => {
      if (window.ethereum) {
        window.ethereum.removeListener('chainChanged', handleChainChanged);
        window.ethereum.removeListener('accountsChanged', handleAccountsChanged);
      }
    };
  }, []);

  React.useEffect(() => {
    if (isConnected && torexAddr) {
      validateTorexAndFetchTokenInfo();
      console.log('isConnected', isConnected);
      console.log('torexAddr', torexAddr);
    }
  }, [isConnected, torexAddr]);

  const MACRO_FORWARDER_ADDRESS = '0xfD01285b9435bc45C243E5e7F978E288B2912de6';
  const SB_MACRO_ADDRESS = '0x383329703f346d72F4b86111a502daaa8f2c69C7'; // Example: Optimism Mainnet

  const macroForwarderABI = [
    'function runMacro(address macro, bytes memory params) external',
  ];

  const sbMacroABI = [
    'function getParams(address torexAddr, int96 flowRate, address distributor, address referrer, uint256 upgradeAmount) public pure returns (bytes memory)',
  ];

  const torexABI = [
    'function getPairedTokens() external view returns (address inToken, address outToken)',
  ];

  const superTokenABI = [
    'function getUnderlyingToken() external view returns (address)',
  ];

  const erc20ABI = [
    'function balanceOf(address account) external view returns (uint256)',
    'function allowance(address owner, address spender) external view returns (uint256)',
    'function approve(address spender, uint256 amount) external returns (bool)',
  ];


  const checkIfWalletIsConnected = async () => {
    if (window.ethereum) {
      try {
        const accounts = await window.ethereum.request({ method: 'eth_accounts' });
        if (accounts.length > 0) {
          setIsConnected(true);
          setWalletAddress(accounts[0]);
          const chainId = await window.ethereum.request({ method: 'eth_chainId' });
          setChainId(parseInt(chainId, 16));
        }
      } catch (error) {
        console.error("An error occurred while checking the wallet connection:", error);
      }
    }
  };

  const connectWallet = async () => {
    if (window.ethereum) {
      try {
        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
        setIsConnected(true);
        setWalletAddress(accounts[0]);
        const chainId = await window.ethereum.request({ method: 'eth_chainId' });
        setChainId(parseInt(chainId, 16));
      } catch (error) {
        console.error("User denied account access");
      }
    } else {
      alert("Please install MetaMask!");
    }
  };

  const handleChainChanged = (chainId) => {
    setChainId(parseInt(chainId, 16));
  };

  const handleAccountsChanged = (accounts) => {
    if (accounts.length > 0) {
      setWalletAddress(accounts[0]);
      setIsConnected(true);
    } else {
      setWalletAddress('');
      setIsConnected(false);
    }
  };

  const validateTorexAndFetchTokenInfo = async () => {
    if (!window.ethereum || !isConnected || !torexAddr) return;
    
    const provider = new ethers.BrowserProvider(window.ethereum);

    try {
      const torex = new ethers.Contract(torexAddr, torexABI, provider);
      const [inTokenAddr] = await torex.getPairedTokens();
      setInTokenAddress(inTokenAddr);
      console.log('inTokenAddr', inTokenAddr); 

      const superToken = new ethers.Contract(inTokenAddr, superTokenABI, provider);
      const underlyingAddr = await superToken.getUnderlyingToken();
      setUnderlyingTokenAddress(underlyingAddr);

      setIsTorexValid(true);
      await fetchBalanceAndAllowance(underlyingAddr,inTokenAddr);
    } catch (error) {
      console.error("Error validating Torex address:", error);
      setIsTorexValid(false);
      setStatus("Invalid Torex address");
    }
  };

  const fetchBalanceAndAllowance = async (tokenAddress, superTokenAddress) => {
    const provider = new ethers.BrowserProvider(window.ethereum);
    //console.log('inTokenAddr', inTokenAddr);
    try {
      if (tokenAddress === ethers.ZeroAddress) {
        // Native token (ETH)
        const balance = await provider.getBalance(walletAddress);
        setMaxBalance(ethers.formatEther(balance));
        setAllowance(null);  // No allowance needed for native token
      } else {
        // ERC20 token
        const erc20 = new ethers.Contract(tokenAddress, erc20ABI, provider);
        console.log(tokenAddress);
        const balance = await erc20.balanceOf(walletAddress);
        console.log(balance);
        console.log(superTokenAddress);
        const allowance = await erc20.allowance(walletAddress, superTokenAddress);
        setMaxBalance(ethers.formatUnits(balance,6));
        setAllowance(ethers.formatEther(allowance));
      }
    } catch (error) {
      console.error("Error fetching balance and allowance:", error);
    }
  };

  const handleUpgradeAmountChange = (e) => {
    const value = e.target.value;
    setUpgradeAmount(value);
    if (value.toLowerCase() === 'max' && maxBalance) {
      setUpgradeAmount(maxBalance);
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus('Processing...');

    try {
      if (!window.ethereum) throw new Error('No crypto wallet found');

      const provider = new ethers.BrowserProvider(window.ethereum);
      const signer = await provider.getSigner();

      const macroForwarder = new ethers.Contract(MACRO_FORWARDER_ADDRESS, macroForwarderABI, signer);
      const sbMacro = new ethers.Contract(SB_MACRO_ADDRESS, sbMacroABI, provider);

      const flowRateBN = ethers.parseEther(flowRate);
      const upgradeAmountBN = ethers.parseEther(upgradeAmount);
      console.log('upgradeAmountBN', upgradeAmountBN); 

      // Check allowance
      if (allowance !== null && BigInt(upgradeAmountBN) > BigInt(ethers.parseEther(allowance))) {
        if (underlyingTokenAddress !== ethers.ZeroAddress) {
          const erc20 = new ethers.Contract(underlyingTokenAddress, erc20ABI, signer);
          const approveTx = await erc20.approve(inTokenAddress, upgradeAmountBN);
          await approveTx.wait();
          setStatus('Approval successful. Starting DCA position.');
        }
      }

      const params = await sbMacro.getParams(
        torexAddr,
        flowRateBN,
        distributor || ethers.ZeroAddress,
        referrer || ethers.ZeroAddress,
        upgradeAmountBN
      );

      const tx = await macroForwarder.runMacro(SB_MACRO_ADDRESS, params);
      await tx.wait();

      setStatus('DCA position started successfully!');
    } catch (err) {
      console.error(err);
      setStatus(`Error: ${err.message}`);
    }
  };

  const inputStyle = {
    width: '100%',
    padding: '8px',
    margin: '8px 0',
    boxSizing: 'border-box',
    borderRadius: '4px',
    border: '1px solid #ccc',
  };

  const buttonStyle = {
    width: '100%',
    padding: '10px',
    backgroundColor: '#4CAF50',
    color: 'white',
    border: 'none',
    borderRadius: '4px',
    cursor: 'pointer',
    marginTop: '10px',
  };

  const formStyle = {
    maxWidth: '400px',
    margin: '0 auto',
    padding: '20px',
    boxShadow: '0 0 10px rgba(0,0,0,0.1)',
    borderRadius: '8px',
  };

  const disabledInputStyle = {
    ...inputStyle,
    backgroundColor: '#f0f0f0',
    cursor: 'not-allowed',
  };

  return (
    <div style={formStyle}>
      <h2 style={{ textAlign: 'center' }}>Start SuperBoring DCA Position</h2>
      {!isConnected ? (
        <button onClick={connectWallet} style={buttonStyle}>Connect Wallet</button>
      ) : (
        <p>Connected Wallet: {walletAddress}</p>
      )}
      {chainId && chainId !== 10 && (
        <p style={{ color: 'red' }}>Chain not supported, please switch to Optimism</p>
      )}
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="torexAddr">Torex Address</label>
          <input
            type="text"
            id="torexAddr"
            value={torexAddr}
            onChange={(e) => setTorexAddr(e.target.value)}
            style={isConnected ? inputStyle : disabledInputStyle}
            required
            disabled={!isConnected}
          />
        </div>
        <div>
          <label htmlFor="flowRate">Flow Rate (in tokens per second)</label>
          <input
            type="text"
            id="flowRate"
            value={flowRate}
            onChange={(e) => setFlowRate(e.target.value)}
            style={isConnected && isTorexValid ? inputStyle : disabledInputStyle}
            required
            disabled={!isConnected || !isTorexValid}
          />
        </div>
        <div>
          <label htmlFor="distributor">Distributor (optional)</label>
          <input
            type="text"
            id="distributor"
            value={distributor}
            onChange={(e) => setDistributor(e.target.value)}
            style={isConnected && isTorexValid ? inputStyle : disabledInputStyle}
            disabled={!isConnected || !isTorexValid}
          />
        </div>
        <div>
          <label htmlFor="referrer">Referrer (optional)</label>
          <input
            type="text"
            id="referrer"
            value={referrer}
            onChange={(e) => setReferrer(e.target.value)}
            style={isConnected && isTorexValid ? inputStyle : disabledInputStyle}
            disabled={!isConnected || !isTorexValid}
          />
        </div>
        <div>
          <label htmlFor="upgradeAmount">Upgrade Amount (in tokens, or 'max')</label>
          <input
            type="text"
            id="upgradeAmount"
            value={upgradeAmount}
            onChange={handleUpgradeAmountChange}
            style={isConnected && isTorexValid ? inputStyle : disabledInputStyle}
            required
            disabled={!isConnected || !isTorexValid}
          />
          {maxBalance && <p>Max available: {maxBalance}</p>}
        </div>
        <button type="submit" style={buttonStyle} disabled={!isConnected || chainId !== 10 || !isTorexValid}>
          Start DCA Position
        </button>
      </form>
      {status && (
        <div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f0f0f0', borderRadius: '4px' }}>
          <strong>Status:</strong> {status}
        </div>
      )}
    </div>
  );
}
Result
Loading...

This JSX code demonstrates how to interact with the MacroForwarder and SBMacro contracts to start a SuperBoring DCA flow. It uses the getParams function from the SBMacro contract to properly encode the parameters, then calls the runMacro function on the MacroForwarder contract.

  1. Make sure to replace the SB_MACRO_ADDRESS with the correct address for the network you're using.
  2. The torexAddr should be the address of the TOREX contract you want to interact with.
  3. The flowRate is the rate at which you want to stream tokens to the Torex contract.
  4. Set distributor and referrer to ethers.ZeroAddress if you don't want to specify these.
  5. Setting upgradeAmount to ethers.constants.MaxUint256 will upgrade the maximum possible amount of tokens. You can also specify a specific amount if needed (eg. the full balance of the wallet in ERC-20)
What is Upgrading?

Upgrading is the process of converting an ERC20 token to a Super Token.

In the codebase of Superfluid, the term "upgrade" refers to the process of wrapping an ERC20 token into a Super Token. The term "downgrade" refers to the process of unwrapping a Super Token back into an ERC20 token.

Super Tokens are ERC20 tokens that have additional functionality, such as streaming and distributions. You can learn more about Super Tokens at the Superfluid Docs.

Before upgrading

Make sure that the user has enough balance and allowance for the upgrade. If the user doesn't have enough balance or allowance, the transaction will revert.

You can check the user's balance and allowance using the ERC20 token's balanceOf and allowance functions.

Will this work for starting, updating and stopping DCA flows?

The SBMacro contract can be used to start, update DCA flows.

In order to stop a DCA flow, you should use the CFAv1Forwarder contract available on all networks, and call the method deteleFlow with the correct parameters.