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.
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
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.
Smart Contract Integration
To integrate SuperBoring at the smart contract level, you'll need to interact with the MacroForwarder contract, which will then call the SBMacro contract.
Example Solidity Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IMacroForwarder {
function runMacro(address macro, bytes memory params) external;
}
contract SuperBoringIntegration {
IMacroForwarder public immutable macroForwarder;
address public immutable sbMacroAddress;
constructor(address _macroForwarder, address _sbMacroAddress) {
macroForwarder = IMacroForwarder(_macroForwarder);
sbMacroAddress = _sbMacroAddress;
}
/**
* @dev A function which start or updates a SuperBoring DCA flow.
* @param torexAddr address of the Torex contract. The token address is derived from this (inToken).
* @param flowRate flowrate to be set for the flow to the Torex contract. The pre-existing flowrate must be 0 (no flow).
* @param distributor address of the distributor, or zero address if none.
* @param referrer address of the referrer, or zero address if none.
* @param upgradeAmount amount (18 decimals) to upgrade from underlying ERC20 to SuperToken.
* - if `type(uint256).max`, the maximum possible amount is upgraded (current allowance).
* - otherwise, the specified amount is upgraded. Requires sufficient underlying balance and allowance, otherwise the transaction will revert.
* Note that upgradeAmount shall be 0 if inToken has no underlying ERC20 token.
*/
function startSuperBoringDCA(
address torexAddr,
int96 flowRate,
address distributor,
address referrer,
uint256 upgradeAmount
) external {
bytes memory params = abi.encode(torexAddr, flowRate, distributor, referrer, upgradeAmount);
macroForwarder.runMacro(sbMacroAddress, params);
}
}
In this example, we create a contract that allows users to start a SuperBoring DCA flow by calling the startSuperBoringDCA
function.
This function encodes the parameters and calls the MacroForwarder's runMacro
function with the SBMacro address and encoded parameters.
As you can see the SBMacro
contract takes the following parameters:
torexAddr
: this should be the address of the TOREX contract you want to interact with. You can find a list of TOREX contracts here.flowRate
: the rate at which you want to stream tokens to the Torex contract. It is in wei/second.distributor
: the address of the distributor, oraddress(0)
if none. To learn more about what is a distributor, check the here.referrer
: the address of the referrer, oraddress(0)
if none. To learn more about referrals, check the here.upgradeAmount
: the amount (in wei) to upgrade from the underlying ERC20 to SuperToken.
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.
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.
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.
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
// 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(); } }, [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); const superToken = new ethers.Contract(inTokenAddr, superTokenABI, provider); const underlyingAddr = await superToken.getUnderlyingToken(); setUnderlyingTokenAddress(underlyingAddr); setIsTorexValid(true); await fetchBalanceAndAllowance(underlyingAddr); } catch (error) { console.error("Error validating Torex address:", error); setIsTorexValid(false); setStatus("Invalid Torex address"); } }; const fetchBalanceAndAllowance = async (tokenAddress) => { const provider = new ethers.BrowserProvider(window.ethereum); 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); const balance = await erc20.balanceOf(walletAddress); console.log(balance); const allowance = await erc20.allowance(walletAddress, SB_MACRO_ADDRESS); setMaxBalance(ethers.formatEther(balance)); 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 && ethers.upgradeAmountBN.gt(ethers.parseEther(allowance))) { if (underlyingTokenAddress !== ethers.ZeroAddress) { const erc20 = new ethers.Contract(underlyingTokenAddress, erc20ABI, signer); const approveTx = await erc20.approve(SB_MACRO_ADDRESS, 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> ); }
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.
- Make sure to replace the
SB_MACRO_ADDRESS
with the correct address for the network you're using. - The
torexAddr
should be the address of the TOREX contract you want to interact with. - The
flowRate
is the rate at which you want to stream tokens to the Torex contract. - Set
distributor
andreferrer
toethers.ZeroAddress
if you don't want to specify these. - Setting
upgradeAmount
toethers.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)
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.
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.
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.