This repository shows how to implement custom SuperTokens.
A custom SuperToken contract typically consists of:
- (immutable) proxy contract with custom logic
- (upgradable) logic contract containing ERC20 and SuperToken functionality
By convention, SuperToken contracts are instances of UUPSProxy. A custom Super Token has custom logic added to this proxy contract.
PureSuperToken.sol is the simplest variant of a custom SuperToken. It's a Pure SuperToken (no underlying ERC20) which has its supply minted on creation.
CustomERC20WrapperProxy.sol shows how a Wrapper SuperToken (has an unerlying ERC20) could be customized.
xchain contains more advanced variants of Custom SuperTokens, suited for cross-chain deployments (e.g. bridging ERC20 <-> SuperToken). See the dedicated section for more.
To set up the repo for development, start by cloning the repo
git clone https://github.com/superfluid-finance/custom-supertokens
cd custom-supertokens
Make sure you have installed Foundry. If you don't have Foundry on your development environment, please refer to Foundry Book.
Once Foundry has been installed, you can run the following command to install dependencies
forge install
This command will install the @superfluid-finance
packages, and the @openzeppelin-contracts (v4.9.3)
packages, in addition to to forge-std
.
As an example of creating your own Custom Super Token, we can take a look at the PureSuperToken.sol
contract.
contract PureSuperTokenProxy is CustomSuperTokenBase, UUPSProxy {
// This shall be invoked exactly once after deployment, needed for the token contract to become operational.
function initialize(
ISuperTokenFactory factory,
string memory name,
string memory symbol,
address receiver,
uint256 initialSupply
) external {
// This call to the factory invokes `UUPSProxy.initialize`, which connects the proxy to the canonical SuperToken implementation.
// It also emits an event which facilitates discovery of this token.
ISuperTokenFactory(factory).initializeCustomSuperToken(address(this));
// This initializes the token storage and sets the `initialized` flag of OpenZeppelin Initializable.
// This makes sure that it will revert if invoked more than once.
ISuperToken(address(this)).initialize(
IERC20(address(0)),
18,
name,
symbol
);
// This mints the specified initial supply to the specified receiver.
ISuperToken(address(this)).selfMint(receiver, initialSupply, "");
}
}
This contract simply creates a new UUPSProxy
with a custom initialize
method.
This method calls SuperTokenFactory.initializeCustomSuperToken
(which emits events facilitating discovery of the SuperToken), then mints the full supply of the token to the receiver
.
For more information on the creation of Custom Super Tokens, please refer to the Technical Documentation or the Protocol Wiki.
Once you created your custom logic in the Custom Super Token Contract, you can now go ahead and write tests of your Custom Super Token.
Going back to the previous example of the Pure Super Token contract, we can see that the file including the tests of PureSuperToken
contract is PureSuperToken.t.sol
.
This file contains a deployment of the protocol in the method setUp
as such:
function setUp() public {
vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin);
SuperfluidFrameworkDeployer sfDeployer = new SuperfluidFrameworkDeployer();
sfDeployer.deployTestFramework();
_sf = sfDeployer.getFramework();
}
Then the file contains 2 tests, one of deployment and the other to check the receiver's balance
function testDeploy() public {
_superTokenProxy = new PureSuperTokenProxy();
assert(address(_superTokenProxy) != address(0));
}
function testSuperTokenBalance() public {
_superTokenProxy = new PureSuperTokenProxy();
_superTokenProxy.initialize(
_sf.superTokenFactory,
"TestToken",
"TST",
_OWNER,
1000
);
ISuperToken superToken = ISuperToken(address(_superTokenProxy));
uint balance = superToken.balanceOf(_OWNER);
assert(balance == 1000);
}
To run these tests you can run the test command from Foundry:
forge test
There are multiple ways to manage the deployment of a Custom Super Token. One of them is using the following command line by replacing the proper arguments with your requirements:
forge create --rpc-url <RPC_URL_OF_DESIRED_CHAIN> --private-key <YOUR_PRIVATE_KEY> --etherscan-api-key <YOUR_ETHERSCAN_API_KEY> --verify --via-ir src/PureSuperToken.sol:PureSuperTokenProxy
To learn more about Custom Super Tokens, check the following resources:
xERC20 is a bridge-agnostic protocol which allows token issuers to deploy crosschain native tokens with zero slippage, perfect fungibility, and granular risk settings — all while maintaining ownership of your token contracts..
BridgedSuperToken.sol extends a Pure SuperToken with the xerc20 interface.
The core functions are mint
and burn
. They leverage the hooks selfMint
and selfBurn
provided by the canonical Super Token implementation.
The rest of the logic is mostly about setting and enforcing rate limits per bridge. The limits are defined as the maximum token amount a bridge can mint or burn per 24 hours (rolling time window).
L2's based on the OP / Superchain stack can use the native Standard Bridge for maximum security.
OPBridgedSuperToken.sol allows that by implementing the required ´IOptimismMintableERC20interface. Its
mint()and
burn()match those of IXERC20, but it adds
bridge()(address of the bridge contract),
remoteToken()(address of the token on L1) and
supportsInterface()` (ERC165 interface detection).
Is a plain OpenZeppelin based ERC20 with ERC20Votes extension. It's suitable for multichain token deployments which want an ERC20 representation on L1 and Super Token representations on L2s.