├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── src ├── CustomERC20Wrapper.sol ├── PureSuperToken.sol ├── PureSuperTokenPermit.sol └── xchain │ ├── ArbBridgedSuperToken.sol │ ├── BridgedSuperToken.sol │ ├── HomeERC20.sol │ ├── OPBridgedSuperToken.sol │ └── interfaces │ ├── IArbToken.sol │ ├── IOptimismMintableERC20.sol │ └── IXERC20.sol └── test ├── PureSuperToken.t.sol ├── PureSuperTokenPermit.t.sol └── xchain ├── ArbBridgedSuperTokenTest.t.sol ├── BridgedSuperTokenTest.t.sol └── OPBridgedSuperTokenTest.t.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .DS_Store 4 | build 5 | 6 | out 7 | cache -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/superfluid-protocol-monorepo"] 5 | path = lib/superfluid-protocol-monorepo 6 | url = https://github.com/superfluid-finance/protocol-monorepo 7 | [submodule "lib/openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 10 | [submodule "lib/openzeppelin-contracts-v5"] 11 | path = lib/openzeppelin-contracts-v5 12 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom Super Tokens 2 | 3 | This repository shows how to implement custom SuperTokens. 4 | 5 | A custom SuperToken contract typically consists of: 6 | * (immutable) proxy contract with custom logic 7 | * (upgradable) logic contract containing ERC20 and SuperToken functionality 8 | 9 | By convention, SuperToken contracts are instances of [UUPSProxy](https://github.com/superfluid-finance/protocol-monorepo/blob/dev/packages/ethereum-contracts/contracts/upgradability/UUPSProxy.sol). 10 | A _custom_ Super Token has custom logic added to this proxy contract. 11 | 12 | [PureSuperToken.sol](src/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. 13 | 14 | [PureSuperTokenPermit.sol](src/PureSuperTokenPermit.sol) is is a Pure SuperToken which also implements IERC20Permit. 15 | 16 | [CustomERC20WrapperProxy.sol](src/CustomERC20WrapperProxy.sol) shows how a _Wrapper SuperToken_ (has an unerlying ERC20) could be customized. 17 | 18 | [xchain](src/xchain) contains more advanced variants of Custom SuperTokens, suited for cross-chain deployments (e.g. bridging ERC20 <-> SuperToken). See [the dedicated section](#bridging-with-xerc20) for more. 19 | 20 | ## Setup 21 | 22 | To set up the repo for development, start by cloning the repo 23 | 24 | ```bash 25 | git clone https://github.com/superfluid-finance/custom-supertokens 26 | cd custom-supertokens 27 | ``` 28 | 29 | ## Installing Foundry 30 | 31 | Make sure you have installed Foundry. If you don't have Foundry on your development environment, please refer to [Foundry Book](https://book.getfoundry.sh/). 32 | 33 | ## Install dependencies 34 | 35 | Once Foundry has been installed, you can run the following command to install dependencies 36 | 37 | ```bash 38 | forge install 39 | ``` 40 | This command will install the `@superfluid-finance` packages, and the `@openzeppelin-contracts (v4.9.3)` packages, in addition to to `forge-std`. 41 | 42 | ## Create your Custom Super Token 43 | 44 | As an example of creating your own Custom Super Token, we can take a look at the `PureSuperToken.sol` contract. 45 | 46 | ```solidity 47 | contract PureSuperTokenProxy is CustomSuperTokenBase, UUPSProxy { 48 | // This shall be invoked exactly once after deployment, needed for the token contract to become operational. 49 | function initialize( 50 | ISuperTokenFactory factory, 51 | string memory name, 52 | string memory symbol, 53 | address receiver, 54 | uint256 initialSupply 55 | ) external { 56 | // This call to the factory invokes `UUPSProxy.initialize`, which connects the proxy to the canonical SuperToken implementation. 57 | // It also emits an event which facilitates discovery of this token. 58 | ISuperTokenFactory(factory).initializeCustomSuperToken(address(this)); 59 | 60 | // This initializes the token storage and sets the `initialized` flag of OpenZeppelin Initializable. 61 | // This makes sure that it will revert if invoked more than once. 62 | ISuperToken(address(this)).initialize( 63 | IERC20(address(0)), 64 | 18, 65 | name, 66 | symbol 67 | ); 68 | 69 | // This mints the specified initial supply to the specified receiver. 70 | ISuperToken(address(this)).selfMint(receiver, initialSupply, ""); 71 | } 72 | } 73 | ``` 74 | 75 | This contract simply creates a new `UUPSProxy` with a custom `initialize` method. 76 | This method calls `SuperTokenFactory.initializeCustomSuperToken` (which emits events facilitating discovery of the SuperToken), then mints the full supply of the token to the `receiver`. 77 | 78 | For more information on the creation of Custom Super Tokens, please refer to the [Technical Documentation](https://docs.superfluid.finance/docs/protocol/super-tokens/guides/deploy-super-token/deploy-custom-super-token) or the [Protocol Wiki](https://github.com/superfluid-finance/protocol-monorepo/wiki/About-Custom-Super-Token). 79 | 80 | ## Test your Custom Super Token Contract 81 | 82 | 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. 83 | 84 | 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`. 85 | 86 | This file contains a deployment of the protocol in the method `setUp` as such: 87 | 88 | ```solidity 89 | function setUp() public { 90 | vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin); 91 | SuperfluidFrameworkDeployer sfDeployer = new SuperfluidFrameworkDeployer(); 92 | sfDeployer.deployTestFramework(); 93 | _sf = sfDeployer.getFramework(); 94 | } 95 | ``` 96 | 97 | Then the file contains 2 tests, one of deployment and the other to check the receiver's balance 98 | 99 | ```solidity 100 | function testDeploy() public { 101 | _superTokenProxy = new PureSuperTokenProxy(); 102 | assert(address(_superTokenProxy) != address(0)); 103 | } 104 | 105 | function testSuperTokenBalance() public { 106 | _superTokenProxy = new PureSuperTokenProxy(); 107 | _superTokenProxy.initialize( 108 | _sf.superTokenFactory, 109 | "TestToken", 110 | "TST", 111 | _OWNER, 112 | 1000 113 | ); 114 | ISuperToken superToken = ISuperToken(address(_superTokenProxy)); 115 | uint balance = superToken.balanceOf(_OWNER); 116 | assert(balance == 1000); 117 | } 118 | ``` 119 | 120 | To run these tests you can run the test command from Foundry: 121 | 122 | ```bash 123 | forge test 124 | ``` 125 | 126 | ## Deployment 127 | 128 | 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: 129 | 130 | ```bash 131 | forge create --rpc-url --private-key --etherscan-api-key --verify --via-ir src/PureSuperToken.sol:PureSuperTokenProxy 132 | ``` 133 | 134 | ## Learn more about Custom Super Tokens 135 | 136 | To learn more about Custom Super Tokens, check the following resources: 137 | 138 | - [The Custom Super Token Wiki](https://github.com/superfluid-finance/protocol-monorepo/wiki/About-Custom-Super-Token) 139 | - [Deploy a Custom Super Token Guide](https://docs.superfluid.finance/docs/protocol/super-tokens/guides/deploy-super-token/deploy-custom-super-token) 140 | 141 | ## Bridging with xERC20 142 | 143 | [xERC20](https://www.xerc20.com/) 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._. 144 | 145 | [BridgedSuperToken.sol](src/xchain/BridgedSuperToken.sol) extends a Pure SuperToken with the xerc20 interface. 146 | The core functions are `mint` and `burn`. They leverage the hooks `selfMint` and `selfBurn` provided by the canonical Super Token implementation. 147 | 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). 148 | 149 | ### HomeERC20 150 | 151 | Is a plain OpenZeppelin based ERC20 with ERC20Votes extension. 152 | It's suitable for multichain token deployments which want an ERC20 representation on L1 and Super Token representations on L2s. 153 | 154 | TODO: for tokens which shall be bridged to Arbitrum, add self-registration with the Arbitrum Gateway. 155 | 156 | ### Optimism / Superchain Standard Bridge 157 | 158 | L2's based on the OP / Superchain stack can use the native [Standard Bridge](https://docs.optimism.io/builders/app-developers/bridging/standard-bridge) for maximum security. 159 | 160 | [OPBridgedSuperToken.sol](src/xchain/OPBridgedSuperToken.sol) allows that by implementing the required ´IOptimismMintableERC20` interface. 161 | 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). 162 | 163 | Note that the L1 OP bridge allows the caller to specify the L2 token to bridge to, so no onchain token _pairing_ is required. 164 | However you may want to make the token available in the canonical bridge UI [superbridge.app](https://superbridge.app/) by making a PR to [its tokenlist repo](https://github.com/superbridgeapp/token-lists/). 165 | 166 | ### Arbitrum Bridge 167 | 168 | We need to use the [generic custom gateway](https://docs.arbitrum.io/build-decentralized-apps/token-bridging/token-bridge-erc20#the-arbitrum-generic-custom-gateway). 169 | Note that the L1 token needs to register itself by calling `L1CustomGateway.registerTokenToL2`. If that's not possible, a DAO gov action is needed for registration. 170 | 171 | The L2 token needs to implement [IArbToken](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/IArbToken.sol). 172 | 173 | 174 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | solc_version = "0.8.26" 3 | src = "src" 4 | out = "out" 5 | libs = ["lib"] 6 | 7 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 8 | 9 | remappings = [ 10 | "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts", 11 | "@superfluid-finance/=lib/superfluid-protocol-monorepo/packages", 12 | "@openzeppelin-v5/contracts/=lib/openzeppelin-contracts-v5/contracts", 13 | ] -------------------------------------------------------------------------------- /src/CustomERC20Wrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity ^0.8.26; 3 | 4 | // This file contains show how to create a custom ERC20 Wrapper 5 | 6 | // This abstract contract provides storage padding for the proxy 7 | import { CustomSuperTokenBase } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/CustomSuperTokenBase.sol"; 8 | // Implementation of UUPSProxy (see https://eips.ethereum.org/EIPS/eip-1822) 9 | import { UUPSProxy } from "@superfluid-finance/ethereum-contracts/contracts/upgradability/UUPSProxy.sol"; 10 | // Superfluid framework interfaces we need 11 | import { ISuperToken, ISuperTokenFactory, IERC20Metadata } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; 12 | 13 | /// @title The Proxy contract for a Custom ERC20 Wrapper 14 | contract CustomERC20WrapperProxy is CustomSuperTokenBase, UUPSProxy { 15 | // This shall be invoked exactly once after deployment, needed for the token contract to become operational. 16 | function initialize( 17 | IERC20Metadata underlyingToken, 18 | ISuperTokenFactory factory, 19 | string memory name, 20 | string memory symbol 21 | ) external { 22 | // This call to the factory invokes `UUPSProxy.initialize`, which connects the proxy to the canonical SuperToken implementation. 23 | // It also emits an event which facilitates discovery of this token. 24 | ISuperTokenFactory(factory).initializeCustomSuperToken(address(this)); 25 | 26 | // This initializes the token storage and sets the `initialized` flag of OpenZeppelin Initializable. 27 | // This makes sure that it will revert if invoked more than once. 28 | ISuperToken(address(this)).initialize( 29 | underlyingToken, 30 | underlyingToken.decimals(), 31 | name, 32 | symbol 33 | ); 34 | } 35 | 36 | // add custom functionality here... 37 | } 38 | 39 | interface ICustomERC20Wrapper is ISuperToken {} 40 | -------------------------------------------------------------------------------- /src/PureSuperToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity ^0.8.26; 3 | 4 | // This file contains everything we need for a minimal Pure SuperToken. 5 | 6 | // This abstract contract provides storage padding for the proxy 7 | import {CustomSuperTokenBase} from 8 | "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/CustomSuperTokenBase.sol"; 9 | // Implementation of UUPSProxy (see https://eips.ethereum.org/EIPS/eip-1822) 10 | import {UUPSProxy} from "@superfluid-finance/ethereum-contracts/contracts/upgradability/UUPSProxy.sol"; 11 | // Superfluid framework interfaces we need 12 | import { 13 | ISuperToken, 14 | ISuperTokenFactory, 15 | IERC20 16 | } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; 17 | 18 | /// @title The Proxy contract for a Pure SuperToken with preminted initial supply. 19 | contract PureSuperTokenProxy is CustomSuperTokenBase, UUPSProxy { 20 | // This shall be invoked exactly once after deployment, needed for the token contract to become operational. 21 | function initialize( 22 | ISuperTokenFactory factory, 23 | string memory name, 24 | string memory symbol, 25 | address receiver, 26 | uint256 initialSupply 27 | ) external { 28 | // This call to the factory invokes `UUPSProxy.initialize`, which connects the proxy to the canonical SuperToken implementation. 29 | // It also emits an event which facilitates discovery of this token. 30 | ISuperTokenFactory(factory).initializeCustomSuperToken(address(this)); 31 | 32 | // This initializes the token storage and sets the `initialized` flag of OpenZeppelin Initializable. 33 | // This makes sure that it will revert if invoked more than once. 34 | ISuperToken(address(this)).initialize(IERC20(address(0)), 18, name, symbol); 35 | 36 | // This mints the specified initial supply to the specified receiver. 37 | ISuperToken(address(this)).selfMint(receiver, initialSupply, ""); 38 | } 39 | } 40 | 41 | // The token interface is just an alias of ISuperToken 42 | // since we need no custom logic (other than for initialization) in the proxy. 43 | interface IPureSuperToken is ISuperToken {} 44 | -------------------------------------------------------------------------------- /src/PureSuperTokenPermit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity ^0.8.26; 3 | 4 | import {IERC20Permit} from "@openzeppelin-v5/contracts/token/ERC20/extensions/IERC20Permit.sol"; 5 | import {ECDSA} from "@openzeppelin-v5/contracts/utils/cryptography/ECDSA.sol"; 6 | import {EIP712} from "@openzeppelin-v5/contracts/utils/cryptography/EIP712.sol"; 7 | import {Nonces} from "@openzeppelin-v5/contracts/utils/Nonces.sol"; 8 | 9 | import {CustomSuperTokenBase} from 10 | "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/CustomSuperTokenBase.sol"; 11 | import {UUPSProxy} from "@superfluid-finance/ethereum-contracts/contracts/upgradability/UUPSProxy.sol"; 12 | import { 13 | ISuperToken, 14 | ISuperTokenFactory, 15 | IERC20 16 | } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; 17 | import { 18 | ISuperToken, 19 | ISuperTokenFactory, 20 | IERC20 21 | } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; 22 | 23 | /// @title The Proxy contract for a Pure SuperToken with permit and preminted initial supply. 24 | contract PureSuperTokenPermitProxy is CustomSuperTokenBase, UUPSProxy, IERC20Permit, EIP712, Nonces { 25 | bytes32 private constant PERMIT_TYPEHASH = 26 | keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 27 | 28 | /// @dev Permit deadline has expired. 29 | error ERC2612ExpiredSignature(uint256 deadline); 30 | 31 | /// @dev Mismatched signature. 32 | error ERC2612InvalidSigner(address signer, address owner); 33 | constructor(string memory name) EIP712(name, "1") {} 34 | 35 | // This shall be invoked exactly once after deployment, needed for the token contract to become operational. 36 | function initialize( 37 | ISuperTokenFactory factory, 38 | string memory name, 39 | string memory symbol, 40 | address receiver, 41 | uint256 initialSupply 42 | ) external { 43 | // This call to the factory invokes `UUPSProxy.initialize`, which connects the proxy to the canonical SuperToken implementation. 44 | // It also emits an event which facilitates discovery of this token. 45 | ISuperTokenFactory(factory).initializeCustomSuperToken(address(this)); 46 | 47 | // This initializes the token storage and sets the `initialized` flag of OpenZeppelin Initializable. 48 | // This makes sure that it will revert if invoked more than once. 49 | ISuperToken(address(this)).initialize(IERC20(address(0)), 18, name, symbol); 50 | 51 | // This mints the specified initial supply to the specified receiver. 52 | ISuperToken(address(this)).selfMint(receiver, initialSupply, ""); 53 | } 54 | 55 | // ============================ IERC20Permit ============================ 56 | 57 | /** 58 | * @inheritdoc IERC20Permit 59 | */ 60 | function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) 61 | public 62 | virtual 63 | { 64 | if (block.timestamp > deadline) { 65 | revert ERC2612ExpiredSignature(deadline); 66 | } 67 | 68 | bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); 69 | 70 | bytes32 hash = _hashTypedDataV4(structHash); 71 | 72 | address signer = ECDSA.recover(hash, v, r, s); 73 | if (signer != owner) { 74 | revert ERC2612InvalidSigner(signer, owner); 75 | } 76 | 77 | ISuperToken(address(this)).selfApproveFor(owner, spender, value); 78 | } 79 | 80 | /** 81 | * @inheritdoc IERC20Permit 82 | */ 83 | function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) { 84 | return super.nonces(owner); 85 | } 86 | 87 | /** 88 | * @inheritdoc IERC20Permit 89 | */ 90 | // solhint-disable-next-line func-name-mixedcase 91 | function DOMAIN_SEPARATOR() external view virtual returns (bytes32) { 92 | return _domainSeparatorV4(); 93 | } 94 | } 95 | 96 | // The token interface is just an alias of ISuperToken 97 | // since we need no custom logic (other than for initialization) in the proxy. 98 | interface IPureSuperTokenPermit is ISuperToken, IERC20Permit {} 99 | -------------------------------------------------------------------------------- /src/xchain/ArbBridgedSuperToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity 0.8.26; 3 | 4 | import { IArbToken } from "./interfaces/IArbToken.sol"; 5 | import { BridgedSuperTokenProxy, IBridgedSuperToken, IXERC20 } from "./BridgedSuperToken.sol"; 6 | 7 | /** 8 | * @title Extends BridgedSuperTokenProxy with the interface required by the Arbitrum Bridge 9 | */ 10 | contract ArbBridgedSuperTokenProxy is BridgedSuperTokenProxy, IArbToken { 11 | address internal immutable _NATIVE_BRIDGE; 12 | address internal immutable _REMOTE_TOKEN; 13 | 14 | // initializes the immutables and sets max limit for the native bridge 15 | constructor(address nativeBridge_, address remoteToken_) { 16 | _NATIVE_BRIDGE = nativeBridge_; 17 | _REMOTE_TOKEN = remoteToken_; 18 | // the native bridge gets (de facto) unlimited mint/burn allowance 19 | setLimits(nativeBridge_, _MAX_LIMIT, _MAX_LIMIT); 20 | } 21 | 22 | // ===== IArbToken ===== 23 | 24 | /// @inheritdoc IArbToken 25 | function bridgeMint(address account, uint256 amount) external override { 26 | return super.mint(account, amount); 27 | } 28 | 29 | /// @inheritdoc IArbToken 30 | function bridgeBurn(address account, uint256 amount) external override { 31 | return super.burn(account, amount); 32 | } 33 | 34 | /// @inheritdoc IArbToken 35 | function l1Address() external view override returns (address) { 36 | return _REMOTE_TOKEN; 37 | } 38 | } 39 | 40 | interface IArbBridgedSuperToken is IBridgedSuperToken, IArbToken { } -------------------------------------------------------------------------------- /src/xchain/BridgedSuperToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity 0.8.26; 3 | 4 | import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol'; 5 | // This abstract contract provides storage padding for the proxy 6 | import { CustomSuperTokenBase } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/CustomSuperTokenBase.sol"; 7 | // Implementation of UUPSProxy (see https://eips.ethereum.org/EIPS/eip-1822) 8 | import { UUPSProxy } from "@superfluid-finance/ethereum-contracts/contracts/upgradability/UUPSProxy.sol"; 9 | // Superfluid framework interfaces we need 10 | import { ISuperToken, ISuperTokenFactory, IERC20 } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; 11 | import { IXERC20 } from "./interfaces/IXERC20.sol"; 12 | 13 | /** 14 | * @title The Proxy contract for a Pure SuperToken with preminted initial supply and with xERC20 support. 15 | */ 16 | contract BridgedSuperTokenProxy is CustomSuperTokenBase, UUPSProxy, Ownable, IXERC20 { 17 | /// The duration it takes for the limits to fully replenish 18 | uint256 internal constant _DURATION = 1 days; 19 | uint256 internal constant _MAX_LIMIT = type(uint256).max / 2; 20 | 21 | /// Maps bridge address to xERC20 bridge configurations 22 | mapping(address => Bridge) public bridges; 23 | 24 | error IXERC20_NoLockBox(); 25 | error IXERC20_LimitsTooHigh(); 26 | 27 | // This shall be invoked exactly once after deployment, needed for the token contract to become operational. 28 | function initialize( 29 | ISuperTokenFactory factory, 30 | string memory name, 31 | string memory symbol, 32 | address receiver, 33 | uint256 initialSupply 34 | ) external { 35 | // This call to the factory invokes `UUPSProxy.initialize`, which connects the proxy to the canonical SuperToken implementation. 36 | // It also emits an event which facilitates discovery of this token. 37 | ISuperTokenFactory(factory).initializeCustomSuperToken(address(this)); 38 | 39 | // This initializes the token storage and sets the `initialized` flag of OpenZeppelin Initializable. 40 | // This makes sure that it will revert if invoked more than once. 41 | ISuperToken(address(this)).initialize(IERC20(address(0)), 18, name, symbol); 42 | 43 | // This mints the specified initial supply to the specified receiver. 44 | ISuperToken(address(this)).selfMint(receiver, initialSupply, ""); 45 | } 46 | 47 | // ===== IXERC20 ===== 48 | 49 | /// @inheritdoc IXERC20 50 | function setLockbox(address /*lockbox*/) external pure { 51 | // no lockbox support needed 52 | revert IXERC20_NoLockBox(); 53 | } 54 | 55 | /// @inheritdoc IXERC20 56 | function setLimits(address bridge, uint256 mintingLimit, uint256 burningLimit) public onlyOwner { 57 | if (mintingLimit > _MAX_LIMIT || burningLimit > _MAX_LIMIT) { 58 | revert IXERC20_LimitsTooHigh(); 59 | } 60 | _changeMinterLimit(bridge, mintingLimit); 61 | _changeBurnerLimit(bridge, burningLimit); 62 | emit BridgeLimitsSet(mintingLimit, burningLimit, bridge); 63 | } 64 | 65 | /// @inheritdoc IXERC20 66 | function mint(address user, uint256 amount) public virtual { 67 | address bridge = msg.sender; 68 | uint256 currentLimit = mintingCurrentLimitOf(bridge); 69 | if (currentLimit < amount) revert IXERC20_NotHighEnoughLimits(); 70 | bridges[bridge].minterParams.timestamp = block.timestamp; 71 | bridges[bridge].minterParams.currentLimit = currentLimit - amount; 72 | ISuperToken(address(this)).selfMint(user, amount, ""); 73 | } 74 | 75 | /// @inheritdoc IXERC20 76 | function burn(address user, uint256 amount) public virtual { 77 | address bridge = msg.sender; 78 | uint256 currentLimit = burningCurrentLimitOf(bridge); 79 | if (currentLimit < amount) revert IXERC20_NotHighEnoughLimits(); 80 | bridges[bridge].burnerParams.timestamp = block.timestamp; 81 | bridges[bridge].burnerParams.currentLimit = currentLimit - amount; 82 | // in order to enforce user allowance limitations, we first transfer to the bridge 83 | // (fails if not enough allowance) and then let the bridge burn it. 84 | ISuperToken(address(this)).selfTransferFrom(user, bridge, bridge, amount); 85 | ISuperToken(address(this)).selfBurn(bridge, amount, ""); 86 | } 87 | 88 | /// @inheritdoc IXERC20 89 | function mintingMaxLimitOf(address bridge) external view returns (uint256 limit) { 90 | limit = bridges[bridge].minterParams.maxLimit; 91 | } 92 | 93 | /// @inheritdoc IXERC20 94 | function burningMaxLimitOf(address bridge) external view returns (uint256 limit) { 95 | limit = bridges[bridge].burnerParams.maxLimit; 96 | } 97 | 98 | /// @inheritdoc IXERC20 99 | function mintingCurrentLimitOf(address bridge) public view returns (uint256 limit) { 100 | limit = _getCurrentLimit( 101 | bridges[bridge].minterParams.currentLimit, 102 | bridges[bridge].minterParams.maxLimit, 103 | bridges[bridge].minterParams.timestamp, 104 | bridges[bridge].minterParams.ratePerSecond 105 | ); 106 | } 107 | 108 | /// @inheritdoc IXERC20 109 | function burningCurrentLimitOf(address bridge) public view returns (uint256 limit) { 110 | limit = _getCurrentLimit( 111 | bridges[bridge].burnerParams.currentLimit, 112 | bridges[bridge].burnerParams.maxLimit, 113 | bridges[bridge].burnerParams.timestamp, 114 | bridges[bridge].burnerParams.ratePerSecond 115 | ); 116 | } 117 | 118 | // ===== INTERNAL FUNCTIONS ===== 119 | 120 | function _changeMinterLimit(address bridge, uint256 limit) internal { 121 | uint256 oldLimit = bridges[bridge].minterParams.maxLimit; 122 | uint256 currentLimit = mintingCurrentLimitOf(bridge); 123 | bridges[bridge].minterParams.maxLimit = limit; 124 | bridges[bridge].minterParams.currentLimit = _calculateNewCurrentLimit(limit, oldLimit, currentLimit); 125 | bridges[bridge].minterParams.ratePerSecond = limit / _DURATION; 126 | bridges[bridge].minterParams.timestamp = block.timestamp; 127 | } 128 | 129 | function _changeBurnerLimit(address bridge, uint256 limit) internal { 130 | uint256 _oldLimit = bridges[bridge].burnerParams.maxLimit; 131 | uint256 _currentLimit = burningCurrentLimitOf(bridge); 132 | bridges[bridge].burnerParams.maxLimit = limit; 133 | bridges[bridge].burnerParams.currentLimit = _calculateNewCurrentLimit(limit, _oldLimit, _currentLimit); 134 | bridges[bridge].burnerParams.ratePerSecond = limit / _DURATION; 135 | bridges[bridge].burnerParams.timestamp = block.timestamp; 136 | } 137 | 138 | function _calculateNewCurrentLimit(uint256 limit, uint256 oldLimit, uint256 currentLimit) 139 | internal pure 140 | returns (uint256 newCurrentLimit) 141 | { 142 | uint256 difference; 143 | 144 | if (limit <= oldLimit) { 145 | difference = oldLimit - limit; 146 | newCurrentLimit = currentLimit > difference ? currentLimit - difference : 0; 147 | } else { 148 | difference = limit - oldLimit; 149 | newCurrentLimit = currentLimit + difference; 150 | } 151 | } 152 | 153 | function _getCurrentLimit(uint256 currentLimit, uint256 maxLimit, uint256 timestamp, uint256 ratePerSecond) 154 | internal view 155 | returns (uint256 limit) 156 | { 157 | limit = currentLimit; 158 | if (limit == maxLimit) { 159 | return limit; 160 | } else if (timestamp + _DURATION <= block.timestamp) { 161 | // the limit is fully replenished 162 | limit = maxLimit; 163 | } else if (timestamp + _DURATION > block.timestamp) { 164 | // the limit is partially replenished 165 | uint256 timePassed = block.timestamp - timestamp; 166 | uint256 calculatedLimit = limit + (timePassed * ratePerSecond); 167 | limit = calculatedLimit > maxLimit ? maxLimit : calculatedLimit; 168 | } 169 | } 170 | } 171 | 172 | // The token interface is just an alias of ISuperToken 173 | // since we need no custom logic (other than for initialization) in the proxy. 174 | interface IBridgedSuperToken is ISuperToken, IXERC20 {} -------------------------------------------------------------------------------- /src/xchain/HomeERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.26; 3 | 4 | import { ERC20 } from "@openzeppelin-v5/contracts/token/ERC20/ERC20.sol"; 5 | import { ERC20Permit, Nonces } from "@openzeppelin-v5/contracts/token/ERC20/extensions/ERC20Permit.sol"; 6 | import { ERC20Votes } from "@openzeppelin-v5/contracts/token/ERC20/extensions/ERC20Votes.sol"; 7 | 8 | /** 9 | * Stock OpenZeppelin ERC20 with ERC-5805 based Votes extension 10 | */ 11 | contract HomeERC20 is ERC20, ERC20Permit, ERC20Votes { 12 | constructor(string memory name, string memory symbol, address treasury, uint256 initialSupply) 13 | ERC20(name, symbol) ERC20Permit(name) 14 | { 15 | _mint(treasury, initialSupply); 16 | } 17 | 18 | // The following functions are overrides required by Solidity. 19 | 20 | function _update(address from, address to, uint256 value) 21 | internal 22 | override(ERC20, ERC20Votes) 23 | { 24 | super._update(from, to, value); 25 | } 26 | 27 | function nonces(address owner) 28 | public 29 | view 30 | override(ERC20Permit, Nonces) 31 | returns (uint256) 32 | { 33 | return super.nonces(owner); 34 | } 35 | } -------------------------------------------------------------------------------- /src/xchain/OPBridgedSuperToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity 0.8.26; 3 | 4 | import { IOptimismMintableERC20, IERC165 } from "./interfaces/IOptimismMintableERC20.sol"; 5 | import { BridgedSuperTokenProxy, IBridgedSuperToken, IXERC20 } from "./BridgedSuperToken.sol"; 6 | 7 | /** 8 | * @title Extends BridgedSuperTokenProxy with the interface required by the Optimism (Superchain) Standard Bridge 9 | */ 10 | contract OPBridgedSuperTokenProxy is BridgedSuperTokenProxy, IOptimismMintableERC20 { 11 | address internal immutable _NATIVE_BRIDGE; 12 | address internal immutable _REMOTE_TOKEN; 13 | 14 | // initializes the immutables and sets max limit for the native bridge 15 | constructor(address nativeBridge_, address remoteToken_) { 16 | _NATIVE_BRIDGE = nativeBridge_; 17 | _REMOTE_TOKEN = remoteToken_; 18 | // the native bridge gets (de facto) unlimited mint/burn allowance 19 | setLimits(nativeBridge_, _MAX_LIMIT, _MAX_LIMIT); 20 | } 21 | 22 | // ===== IOptimismMintableERC20 ===== 23 | 24 | /// Returns the address of the corresponding token on the home chain 25 | function remoteToken() external view returns (address) { 26 | return _REMOTE_TOKEN; 27 | } 28 | 29 | /// Returns the address of the bridge contract 30 | function bridge() external view returns (address) { 31 | return _NATIVE_BRIDGE; 32 | } 33 | 34 | /// @inheritdoc IXERC20 35 | function mint(address user, uint256 amount) public override(BridgedSuperTokenProxy, IOptimismMintableERC20) { 36 | return super.mint(user, amount); 37 | } 38 | 39 | /// @inheritdoc IXERC20 40 | function burn(address user, uint256 amount) public override(BridgedSuperTokenProxy, IOptimismMintableERC20) { 41 | return super.burn(user, amount); 42 | } 43 | 44 | // ===== IERC165 ===== 45 | 46 | /// ERC165 interface detection 47 | function supportsInterface(bytes4 interfaceId) external pure virtual returns (bool) { 48 | return 49 | interfaceId == type(IERC165).interfaceId || 50 | interfaceId == type(IOptimismMintableERC20).interfaceId; 51 | } 52 | } 53 | 54 | interface IOPBridgedSuperToken is IBridgedSuperToken, IOptimismMintableERC20 { 55 | function mint(address _to, uint256 _amount) external override(IXERC20, IOptimismMintableERC20); 56 | function burn(address _from, uint256 _amount) external override(IXERC20, IOptimismMintableERC20); 57 | } -------------------------------------------------------------------------------- /src/xchain/interfaces/IArbToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | * Copyright 2020, Offchain Labs, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | /** 20 | * @title Minimum expected interface for L2 token that interacts with the L2 token bridge (this is the interface necessary 21 | * for a custom token that interacts with the bridge, see TestArbCustomToken.sol for an example implementation). 22 | * @dev For the token to be compatible out of the box with the tooling available (e.g., the Arbitrum bridge), it is 23 | * recommended to keep the implementation of this interface as close as possible to the `TestArbCustomToken` example. 24 | */ 25 | 26 | // solhint-disable-next-line compiler-version 27 | pragma solidity >=0.6.9 <0.9.0; 28 | 29 | interface IArbToken { 30 | /** 31 | * @notice should increase token supply by amount, and should (probably) only be callable by the L1 bridge. 32 | */ 33 | function bridgeMint(address account, uint256 amount) external; 34 | 35 | /** 36 | * @notice should decrease token supply by amount, and should (probably) only be callable by the L1 bridge. 37 | */ 38 | function bridgeBurn(address account, uint256 amount) external; 39 | 40 | /** 41 | * @return address of layer 1 token 42 | */ 43 | function l1Address() external view returns (address); 44 | } 45 | -------------------------------------------------------------------------------- /src/xchain/interfaces/IOptimismMintableERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 5 | 6 | /// @title IOptimismMintableERC20 7 | /// @notice This interface is available on the OptimismMintableERC20 contract. 8 | /// We declare it as a separate interface so that it can be used in 9 | /// custom implementations of OptimismMintableERC20. 10 | interface IOptimismMintableERC20 is IERC165 { 11 | function remoteToken() external view returns (address); 12 | 13 | function bridge() external returns (address); 14 | 15 | function mint(address _to, uint256 _amount) external; 16 | 17 | function burn(address _from, uint256 _amount) external; 18 | } -------------------------------------------------------------------------------- /src/xchain/interfaces/IXERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity ^0.8.4; 3 | 4 | interface IXERC20 { 5 | /** 6 | * @notice Emits when a lockbox is set 7 | * 8 | * @param _lockbox The address of the lockbox 9 | */ 10 | 11 | event LockboxSet(address _lockbox); 12 | 13 | /** 14 | * @notice Emits when a limit is set 15 | * 16 | * @param _mintingLimit The updated minting limit we are setting to the bridge 17 | * @param _burningLimit The updated burning limit we are setting to the bridge 18 | * @param _bridge The address of the bridge we are setting the limit too 19 | */ 20 | 21 | event BridgeLimitsSet(uint256 _mintingLimit, uint256 _burningLimit, address indexed _bridge); 22 | 23 | /** 24 | * @notice Reverts when a user with too low of a limit tries to call mint/burn 25 | */ 26 | 27 | error IXERC20_NotHighEnoughLimits(); 28 | 29 | struct Bridge { 30 | BridgeParameters minterParams; 31 | BridgeParameters burnerParams; 32 | } 33 | 34 | struct BridgeParameters { 35 | // is set when the limit is set and on mint/burn 36 | uint256 timestamp; 37 | // is set when the limit is set 38 | uint256 ratePerSecond; 39 | // limit at the time of setting it 40 | uint256 maxLimit; 41 | // changed on mint/burn 42 | uint256 currentLimit; 43 | } 44 | 45 | /** 46 | * @notice Sets the lockbox address 47 | * 48 | * @param _lockbox The address of the lockbox (0x0 if no lockbox) 49 | */ 50 | 51 | function setLockbox(address _lockbox) external; 52 | 53 | /** 54 | * @notice Updates the limits of any bridge 55 | * @dev Can only be called by the owner 56 | * @param _mintingLimit The updated minting limit we are setting to the bridge 57 | * @param _burningLimit The updated burning limit we are setting to the bridge 58 | * @param _bridge The address of the bridge we are setting the limits too 59 | */ 60 | function setLimits(address _bridge, uint256 _mintingLimit, uint256 _burningLimit) external; 61 | 62 | /** 63 | * @notice Returns the max limit of a bridge 64 | * 65 | * @param _bridge The bridge we are viewing the limits of 66 | * @return _limit The limit the bridge has 67 | */ 68 | function mintingMaxLimitOf(address _bridge) external view returns (uint256 _limit); 69 | 70 | /** 71 | * @notice Returns the max limit of a bridge 72 | * 73 | * @param _bridge the bridge we are viewing the limits of 74 | * @return _limit The limit the bridge has 75 | */ 76 | 77 | function burningMaxLimitOf(address _bridge) external view returns (uint256 _limit); 78 | 79 | /** 80 | * @notice Returns the current limit of a bridge 81 | * 82 | * @param _bridge The bridge we are viewing the limits of 83 | * @return _limit The limit the bridge has 84 | */ 85 | 86 | function mintingCurrentLimitOf(address _bridge) external view returns (uint256 _limit); 87 | 88 | /** 89 | * @notice Returns the current limit of a bridge 90 | * 91 | * @param _bridge the bridge we are viewing the limits of 92 | * @return _limit The limit the bridge has 93 | */ 94 | 95 | function burningCurrentLimitOf(address _bridge) external view returns (uint256 _limit); 96 | 97 | /** 98 | * @notice Mints tokens for a user 99 | * @dev Can only be called by a bridge 100 | * @param _user The address of the user who needs tokens minted 101 | * @param _amount The amount of tokens being minted 102 | */ 103 | 104 | function mint(address _user, uint256 _amount) external; 105 | 106 | /** 107 | * @notice Burns tokens for a user 108 | * @dev Can only be called by a bridge 109 | * @param _user The address of the user who needs tokens burned 110 | * @param _amount The amount of tokens being burned 111 | */ 112 | 113 | function burn(address _user, uint256 _amount) external; 114 | } -------------------------------------------------------------------------------- /test/PureSuperToken.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.26; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {ISuperToken} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; 6 | import {SuperfluidFrameworkDeployer} from 7 | "@superfluid-finance/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.t.sol"; 8 | import {ERC1820RegistryCompiled} from 9 | "@superfluid-finance/ethereum-contracts/contracts/libs/ERC1820RegistryCompiled.sol"; 10 | import {SuperTokenV1Library} from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; 11 | import {PureSuperTokenProxy, IPureSuperToken} from "../src/PureSuperToken.sol"; 12 | 13 | using SuperTokenV1Library for IPureSuperToken; 14 | 15 | contract PureSuperTokenProxyTest is Test { 16 | address internal constant _OWNER = address(0x1); 17 | uint256 internal constant _INITIAL_SUPPLY = 1000 ether; 18 | address internal constant _ALICE = address(0x4242); 19 | address internal constant _BOB = address(0x4243); 20 | 21 | SuperfluidFrameworkDeployer.Framework internal _sf; 22 | IPureSuperToken internal _superToken; 23 | 24 | function setUp() public { 25 | PureSuperTokenProxy _superTokenProxy; 26 | vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin); 27 | SuperfluidFrameworkDeployer sfDeployer = new SuperfluidFrameworkDeployer(); 28 | sfDeployer.deployTestFramework(); 29 | _sf = sfDeployer.getFramework(); 30 | 31 | // deploy the proxy 32 | PureSuperTokenProxy superTokenProxy = _superTokenProxy = new PureSuperTokenProxy(); 33 | _superTokenProxy.initialize(_sf.superTokenFactory, "TestToken", "TST", _ALICE, _INITIAL_SUPPLY); 34 | _superToken = IPureSuperToken(address(superTokenProxy)); 35 | } 36 | 37 | function testInitialMintBalance() public { 38 | assert(_superToken.balanceOf(_ALICE) == _INITIAL_SUPPLY); 39 | } 40 | 41 | function testFlow() public { 42 | int96 flowRate = 1e12; 43 | uint256 duration = 3600; 44 | 45 | uint256 aliceInitialBalance = _superToken.balanceOf(_ALICE); 46 | assertEq(_superToken.balanceOf(_BOB), 0, "Bob should start with balance 0"); 47 | 48 | vm.startPrank(_ALICE); 49 | _superToken.createFlow(_BOB, flowRate); 50 | vm.stopPrank(); 51 | 52 | vm.warp(block.timestamp + duration); 53 | 54 | uint256 flowAmount = uint96(flowRate) * duration; 55 | assertEq(_superToken.balanceOf(_BOB), flowAmount, "Bob unexpected balance"); 56 | 57 | vm.startPrank(_ALICE); 58 | _superToken.deleteFlow(_ALICE, _BOB); 59 | vm.stopPrank(); 60 | 61 | assertEq(_superToken.balanceOf(_BOB), flowAmount, "Bob unexpected balance"); 62 | assertEq(_superToken.balanceOf(_ALICE), aliceInitialBalance - flowAmount, "Alice unexpected balance"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/PureSuperTokenPermit.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.26; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {ISuperToken} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; 6 | import {SuperfluidFrameworkDeployer} from 7 | "@superfluid-finance/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.t.sol"; 8 | import {ERC1820RegistryCompiled} from 9 | "@superfluid-finance/ethereum-contracts/contracts/libs/ERC1820RegistryCompiled.sol"; 10 | import {PureSuperTokenPermitProxy, IPureSuperTokenPermit} from "../src/PureSuperTokenPermit.sol"; 11 | 12 | contract PureSuperTokenPermitTest is Test { 13 | string internal constant _NAME = "TestToken"; 14 | address internal constant _OWNER = address(0x1); 15 | uint256 internal constant _PERMIT_SIGNER_PK = 0xA11CE; 16 | 17 | address internal _permitSigner; 18 | IPureSuperTokenPermit internal _superTokenPermit; 19 | SuperfluidFrameworkDeployer.Framework internal _sf; 20 | 21 | function setUp() public { 22 | vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin); 23 | SuperfluidFrameworkDeployer sfDeployer = new SuperfluidFrameworkDeployer(); 24 | sfDeployer.deployTestFramework(); 25 | _sf = sfDeployer.getFramework(); 26 | 27 | PureSuperTokenPermitProxy superTokenPermitProxy = new PureSuperTokenPermitProxy(_NAME); 28 | superTokenPermitProxy.initialize(_sf.superTokenFactory, _NAME, "TST", _OWNER, 1000); 29 | _superTokenPermit = IPureSuperTokenPermit(address(superTokenPermitProxy)); 30 | 31 | _permitSigner = vm.addr(_PERMIT_SIGNER_PK); 32 | 33 | vm.prank(_OWNER); 34 | _superTokenPermit.transfer(_permitSigner, 500); 35 | } 36 | 37 | function testPermit(address relayer) public { 38 | address spender = address(0x2); 39 | uint256 value = 100; 40 | uint256 deadline = block.timestamp + 1 hours; 41 | 42 | uint256 nonce = _superTokenPermit.nonces(_permitSigner); 43 | 44 | assertEq(_superTokenPermit.allowance(_permitSigner, spender), 0, "Allowance should be 0"); 45 | 46 | bytes32 digest = _createPermitDigest(_permitSigner, spender, value, nonce, deadline); 47 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_PERMIT_SIGNER_PK, digest); 48 | 49 | vm.startPrank(relayer); 50 | 51 | // expect revert if spender doesn't match 52 | vm.expectRevert(); 53 | _superTokenPermit.permit(_permitSigner, relayer, value, deadline, v, r, s); 54 | 55 | // expect revert if value doesn't match 56 | vm.expectRevert(); 57 | _superTokenPermit.permit(_permitSigner, spender, value + 1, deadline, v, r, s); 58 | 59 | // expect revert if signature is invalid 60 | vm.expectRevert(); 61 | _superTokenPermit.permit(_permitSigner, spender, value, deadline, v + 1, r, s); 62 | 63 | // expect revert if deadline is in the past 64 | uint256 prevBlockTS = block.timestamp; 65 | vm.warp(block.timestamp + deadline + 1); 66 | vm.expectRevert(); 67 | _superTokenPermit.permit(_permitSigner, spender, value, deadline, v, r, s); 68 | 69 | // succeed with correct parameters 70 | vm.warp(prevBlockTS); 71 | _superTokenPermit.permit(_permitSigner, spender, value, deadline, v, r, s); 72 | 73 | vm.stopPrank(); 74 | 75 | // Verify expected state changes 76 | assertEq(_superTokenPermit.nonces(_permitSigner), 1, "Nonce should be incremented"); 77 | assertEq(_superTokenPermit.allowance(_permitSigner, spender), value, "Allowance should be set"); 78 | } 79 | 80 | // ============================ Internal Functions ============================ 81 | 82 | function _createPermitDigest(address owner, address spender, uint256 value, uint256 nonce, uint256 deadline) 83 | internal 84 | view 85 | returns (bytes32) 86 | { 87 | bytes32 PERMIT_TYPEHASH = 88 | keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 89 | 90 | bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline)); 91 | 92 | bytes32 DOMAIN_SEPARATOR = _superTokenPermit.DOMAIN_SEPARATOR(); 93 | 94 | return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/xchain/ArbBridgedSuperTokenTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.26; 3 | 4 | import { ArbBridgedSuperTokenProxy, IArbBridgedSuperToken, IBridgedSuperToken, IArbToken } from "../../src/xchain/ArbBridgedSuperToken.sol"; 5 | import { BridgedSuperTokenTest } from "./BridgedSuperTokenTest.t.sol"; 6 | 7 | contract ArbBridgedSuperTokenTest is BridgedSuperTokenTest { 8 | address internal _nativeBridge = address(99); 9 | address internal _remoteToken = address(98); 10 | IArbBridgedSuperToken internal _arbToken; 11 | 12 | function _deployToken(address owner) internal override { 13 | // deploy proxy 14 | ArbBridgedSuperTokenProxy proxy = new ArbBridgedSuperTokenProxy(_nativeBridge, _remoteToken); 15 | // initialize proxy 16 | proxy.initialize(sf.superTokenFactory, "Test Token", "TT", owner, 1000); 17 | proxy.transferOwnership(owner); 18 | 19 | _arbToken = IArbBridgedSuperToken(address(proxy)); 20 | _xerc20 = IBridgedSuperToken(_arbToken); 21 | } 22 | 23 | function testMintByNativeBridge(uint256 _amount) public { 24 | _amount = bound(_amount, 1, type(uint256).max / 2); 25 | 26 | vm.prank(_nativeBridge); 27 | _arbToken.bridgeMint(_user, _amount); 28 | 29 | assertEq(_xerc20.balanceOf(_user), _amount); 30 | } 31 | 32 | function testBurnByNativeBridge(uint256 _amount) public { 33 | _amount = bound(_amount, 1, type(uint256).max / 2); 34 | 35 | vm.prank(_nativeBridge); 36 | _arbToken.bridgeMint(_user, _amount); 37 | 38 | vm.prank(_user); 39 | _arbToken.approve(_nativeBridge, _amount); 40 | 41 | vm.prank(_nativeBridge); 42 | _arbToken.bridgeBurn(_user, _amount); 43 | 44 | assertEq(_xerc20.balanceOf(_user), 0); 45 | } 46 | } -------------------------------------------------------------------------------- /test/xchain/BridgedSuperTokenTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.26; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | import { ERC1820RegistryCompiled } from "@superfluid-finance/ethereum-contracts/contracts/libs/ERC1820RegistryCompiled.sol"; 6 | import { SuperfluidFrameworkDeployer } from "@superfluid-finance/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.t.sol"; 7 | import { BridgedSuperTokenProxy, IBridgedSuperToken, IXERC20 } from "../../src/xchain/BridgedSuperToken.sol"; 8 | 9 | contract BridgedSuperTokenTest is Test { 10 | SuperfluidFrameworkDeployer.Framework internal sf; 11 | 12 | address internal _owner = address(0x42); 13 | address internal _user = address(0x43); 14 | address internal _minter = address(0x44); 15 | IBridgedSuperToken internal _xerc20; 16 | 17 | function _deployToken(address owner) internal virtual { 18 | // deploy proxy 19 | BridgedSuperTokenProxy proxy = new BridgedSuperTokenProxy(); 20 | // initialize proxy 21 | proxy.initialize(sf.superTokenFactory, "Test Token", "TT", owner, 1000); 22 | proxy.transferOwnership(owner); 23 | 24 | _xerc20 = IBridgedSuperToken(address(proxy)); 25 | } 26 | 27 | function setUp() public virtual { 28 | // deploy SF framework 29 | vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin); 30 | SuperfluidFrameworkDeployer deployer = new SuperfluidFrameworkDeployer(); 31 | deployer.deployTestFramework(); 32 | sf = deployer.getFramework(); 33 | 34 | _deployToken(_owner); 35 | } 36 | 37 | // Test cases are replicated from the reference XERC20 implementation, with small adjustments 38 | 39 | // contract UnitMintBurn 40 | 41 | function testMintRevertsIfNotApprove(uint256 _amount) public { 42 | vm.assume(_amount > 0); 43 | vm.prank(_user); 44 | vm.expectRevert(IXERC20.IXERC20_NotHighEnoughLimits.selector); 45 | _xerc20.mint(_user, _amount); 46 | } 47 | 48 | function testBurnRevertsWhenLimitIsTooLow(uint256 _amount0, uint256 _amount1) public { 49 | _amount0 = bound(_amount0, 1, 1e40); 50 | _amount1 = bound(_amount1, 1, 1e40); 51 | vm.assume(_amount1 > _amount0); 52 | vm.prank(_owner); 53 | _xerc20.setLimits(_user, _amount0, 0); 54 | 55 | vm.startPrank(_user); 56 | _xerc20.mint(_user, _amount0); 57 | vm.expectRevert(IXERC20.IXERC20_NotHighEnoughLimits.selector); 58 | _xerc20.burn(_user, _amount1); 59 | vm.stopPrank(); 60 | } 61 | 62 | function testMint(uint256 _amount) public { 63 | _amount = bound(_amount, 1, type(uint256).max / 2); 64 | 65 | vm.prank(_owner); 66 | _xerc20.setLimits(_user, _amount, 0); 67 | vm.prank(_user); 68 | _xerc20.mint(_minter, _amount); 69 | 70 | assertEq(_xerc20.balanceOf(_minter), _amount); 71 | } 72 | 73 | function testBurn(uint256 _amount) public { 74 | _amount = bound(_amount, 1, 1e40); 75 | vm.startPrank(_owner); 76 | _xerc20.setLimits(_user, _amount, _amount); 77 | vm.stopPrank(); 78 | 79 | vm.startPrank(_user); 80 | 81 | _xerc20.mint(_user, _amount); 82 | _xerc20.burn(_user, _amount); 83 | vm.stopPrank(); 84 | 85 | assertEq(_xerc20.balanceOf(_user), 0); 86 | } 87 | 88 | function testBurnRevertsWithoutApproval(uint256 _amount) public { 89 | _amount = bound(_amount, 1, 1e40); 90 | 91 | vm.prank(_owner); 92 | _xerc20.setLimits(_owner, _amount, _amount); 93 | 94 | vm.startPrank(_owner); 95 | vm.expectRevert(); 96 | _xerc20.burn(_user, _amount); 97 | vm.stopPrank(); 98 | 99 | assertEq(_xerc20.balanceOf(_user), 0); 100 | } 101 | 102 | function testBurnReducesAllowance(uint256 _amount, uint256 _approvalAmount) public { 103 | _amount = bound(_amount, 1, 1e40); 104 | _approvalAmount = bound(_approvalAmount, _amount, 1e45); 105 | 106 | vm.prank(_owner); 107 | _xerc20.setLimits(_minter, _amount, _amount); 108 | 109 | vm.prank(_user); 110 | _xerc20.approve(_minter, _approvalAmount); 111 | 112 | vm.startPrank(_minter); 113 | _xerc20.mint(_user, _amount); 114 | _xerc20.burn(_user, _amount); 115 | vm.stopPrank(); 116 | 117 | assertEq(_xerc20.allowance(_user, _minter), _approvalAmount - _amount); 118 | } 119 | 120 | // contract UnitCreateParams 121 | function testChangeLimit(uint256 _amount, address _randomAddr) public { 122 | _amount = bound(_amount, 0, type(uint256).max / 2); 123 | vm.assume(_randomAddr != address(0)); 124 | vm.startPrank(_owner); 125 | _xerc20.setLimits(_randomAddr, _amount, _amount); 126 | vm.stopPrank(); 127 | assertEq(_xerc20.mintingMaxLimitOf(_randomAddr), _amount); 128 | assertEq(_xerc20.burningMaxLimitOf(_randomAddr), _amount); 129 | } 130 | 131 | function testRevertsWithWrongCaller() public { 132 | vm.expectRevert('Ownable: caller is not the owner'); 133 | _xerc20.setLimits(_minter, 1e18, 0); 134 | } 135 | 136 | function testAddingMintersAndLimits( 137 | uint256 _amount0, 138 | uint256 _amount1, 139 | uint256 _amount2, 140 | address _user0, 141 | address _user1, 142 | address _user2 143 | ) public { 144 | _amount0 = bound(_amount0, 1, type(uint256).max / 2); 145 | _amount1 = bound(_amount1, 1, type(uint256).max / 2); 146 | _amount2 = bound(_amount2, 1, type(uint256).max / 2); 147 | 148 | vm.assume(_user0 != _user1 && _user1 != _user2 && _user0 != _user2); 149 | uint256[] memory _limits = new uint256[](3); 150 | address[] memory _minters = new address[](3); 151 | 152 | _limits[0] = _amount0; 153 | _limits[1] = _amount1; 154 | _limits[2] = _amount2; 155 | 156 | _minters[0] = _user0; 157 | _minters[1] = _user1; 158 | _minters[2] = _user2; 159 | 160 | vm.startPrank(_owner); 161 | for (uint256 _i = 0; _i < _minters.length; _i++) { 162 | _xerc20.setLimits(_minters[_i], _limits[_i], _limits[_i]); 163 | } 164 | vm.stopPrank(); 165 | 166 | assertEq(_xerc20.mintingMaxLimitOf(_user0), _amount0); 167 | assertEq(_xerc20.mintingMaxLimitOf(_user1), _amount1); 168 | assertEq(_xerc20.mintingMaxLimitOf(_user2), _amount2); 169 | assertEq(_xerc20.burningMaxLimitOf(_user0), _amount0); 170 | assertEq(_xerc20.burningMaxLimitOf(_user1), _amount1); 171 | assertEq(_xerc20.burningMaxLimitOf(_user2), _amount2); 172 | } 173 | 174 | function testchangeBridgeMintingLimitEmitsEvent(uint256 _limit) public { 175 | _limit = bound(_limit, 0, type(uint256).max / 2); 176 | vm.prank(_owner); 177 | vm.expectEmit(true, true, true, true); 178 | emit IXERC20.BridgeLimitsSet(_limit, 0, _minter); 179 | _xerc20.setLimits(_minter, _limit, 0); 180 | } 181 | 182 | function testchangeBridgeBurningLimitEmitsEvent(uint256 _limit) public { 183 | _limit = bound(_limit, 0, type(uint256).max / 2); 184 | vm.prank(_owner); 185 | vm.expectEmit(true, true, true, true); 186 | emit IXERC20.BridgeLimitsSet(0, _limit, _minter); 187 | _xerc20.setLimits(_minter, 0, _limit); 188 | } 189 | 190 | function testSettingLimitsToUnapprovedUser(uint256 _amount) public { 191 | _amount = bound(_amount, 1, type(uint256).max / 2); 192 | 193 | vm.startPrank(_owner); 194 | _xerc20.setLimits(_minter, _amount, _amount); 195 | vm.stopPrank(); 196 | 197 | assertEq(_xerc20.mintingMaxLimitOf(_minter), _amount); 198 | assertEq(_xerc20.burningMaxLimitOf(_minter), _amount); 199 | } 200 | 201 | function testUseLimitsUpdatesLimit(uint256 _limit) public { 202 | _limit = bound(_limit, 1e6, type(uint256).max / 2); 203 | vm.warp(1_683_145_698); // current timestamp at the time of testing 204 | 205 | vm.startPrank(_owner); 206 | _xerc20.setLimits(_minter, _limit, _limit); 207 | vm.stopPrank(); 208 | 209 | vm.startPrank(_minter); 210 | _xerc20.mint(_minter, _limit); 211 | _xerc20.burn(_minter, _limit); 212 | vm.stopPrank(); 213 | 214 | assertEq(_xerc20.mintingMaxLimitOf(_minter), _limit); 215 | assertEq(_xerc20.mintingCurrentLimitOf(_minter), 0); 216 | assertEq(_xerc20.burningMaxLimitOf(_minter), _limit); 217 | assertEq(_xerc20.burningCurrentLimitOf(_minter), 0); 218 | } 219 | 220 | function testCurrentLimitIsMaxLimitIfUnused(uint256 _limit) public { 221 | _limit = bound(_limit, 0, type(uint256).max / 2); 222 | uint256 _currentTimestamp = 1_683_145_698; 223 | vm.warp(_currentTimestamp); 224 | 225 | vm.startPrank(_owner); 226 | _xerc20.setLimits(_minter, _limit, _limit); 227 | vm.stopPrank(); 228 | 229 | vm.warp(_currentTimestamp + 12 hours); 230 | 231 | assertEq(_xerc20.mintingCurrentLimitOf(_minter), _limit); 232 | assertEq(_xerc20.burningCurrentLimitOf(_minter), _limit); 233 | } 234 | 235 | function testCurrentLimitIsMaxLimitIfOver24Hours(uint256 _limit) public { 236 | _limit = bound(_limit, 0, type(uint256).max / 2); 237 | uint256 _currentTimestamp = 1_683_145_698; 238 | vm.warp(_currentTimestamp); 239 | 240 | vm.startPrank(_owner); 241 | _xerc20.setLimits(_minter, _limit, _limit); 242 | vm.stopPrank(); 243 | 244 | vm.startPrank(_minter); 245 | _xerc20.mint(_minter, _limit); 246 | _xerc20.burn(_minter, _limit); 247 | vm.stopPrank(); 248 | 249 | vm.warp(_currentTimestamp + 30 hours); 250 | 251 | assertEq(_xerc20.mintingCurrentLimitOf(_minter), _limit); 252 | assertEq(_xerc20.burningCurrentLimitOf(_minter), _limit); 253 | } 254 | 255 | function testLimitVestsLinearly(uint256 _limit) public { 256 | _limit = bound(_limit, 1e6, type(uint256).max / 2); 257 | uint256 _currentTimestamp = 1_683_145_698; 258 | vm.warp(_currentTimestamp); 259 | 260 | vm.startPrank(_owner); 261 | _xerc20.setLimits(_minter, _limit, _limit); 262 | vm.stopPrank(); 263 | 264 | vm.startPrank(_minter); 265 | _xerc20.mint(_minter, _limit); 266 | _xerc20.burn(_minter, _limit); 267 | vm.stopPrank(); 268 | 269 | vm.warp(_currentTimestamp + 12 hours); 270 | 271 | assertApproxEqRel(_xerc20.mintingCurrentLimitOf(_minter), _limit / 2, 0.1 ether); 272 | assertApproxEqRel(_xerc20.burningCurrentLimitOf(_minter), _limit / 2, 0.1 ether); 273 | } 274 | 275 | function testOverflowLimitMakesItMax(uint256 _limit, uint256 _usedLimit) public { 276 | _limit = bound(_limit, 1e6, 100_000_000_000_000e18); 277 | vm.assume(_usedLimit < 1e3); 278 | uint256 _currentTimestamp = 1_683_145_698; 279 | vm.warp(_currentTimestamp); 280 | 281 | vm.startPrank(_owner); 282 | _xerc20.setLimits(_minter, _limit, _limit); 283 | vm.stopPrank(); 284 | 285 | vm.startPrank(_minter); 286 | _xerc20.mint(_minter, _usedLimit); 287 | _xerc20.burn(_minter, _usedLimit); 288 | vm.stopPrank(); 289 | 290 | vm.warp(_currentTimestamp + 20 hours); 291 | 292 | assertEq(_xerc20.mintingCurrentLimitOf(_minter), _limit); 293 | assertEq(_xerc20.burningCurrentLimitOf(_minter), _limit); 294 | } 295 | 296 | function testchangeBridgeMintingLimitIncreaseCurrentLimitByTheDifferenceItWasChanged( 297 | uint256 _limit, 298 | uint256 _usedLimit 299 | ) public { 300 | vm.assume(_limit < 1e40); 301 | vm.assume(_usedLimit < 1e3); 302 | vm.assume(_limit > _usedLimit); 303 | uint256 _currentTimestamp = 1_683_145_698; 304 | vm.warp(_currentTimestamp); 305 | 306 | vm.startPrank(_owner); 307 | _xerc20.setLimits(_minter, _limit, _limit); 308 | vm.stopPrank(); 309 | 310 | vm.startPrank(_minter); 311 | _xerc20.mint(_minter, _usedLimit); 312 | _xerc20.burn(_minter, _usedLimit); 313 | vm.stopPrank(); 314 | 315 | vm.startPrank(_owner); 316 | // Adding 100k to the limit 317 | _xerc20.setLimits(_minter, _limit + 100_000, _limit + 100_000); 318 | vm.stopPrank(); 319 | 320 | assertEq(_xerc20.mintingCurrentLimitOf(_minter), (_limit - _usedLimit) + 100_000); 321 | } 322 | 323 | function testchangeBridgeMintingLimitDecreaseCurrentLimitByTheDifferenceItWasChanged( 324 | uint256 _limit, 325 | uint256 _usedLimit 326 | ) public { 327 | uint256 _currentTimestamp = 1_683_145_698; 328 | vm.warp(_currentTimestamp); 329 | _limit = bound(_limit, 1e15, 1e40); 330 | _usedLimit = bound(_usedLimit, 100_000, 1e9); 331 | 332 | vm.startPrank(_owner); 333 | // Setting the limit at its original limit 334 | _xerc20.setLimits(_minter, _limit, _limit); 335 | vm.stopPrank(); 336 | 337 | vm.startPrank(_minter); 338 | _xerc20.mint(_minter, _usedLimit); 339 | _xerc20.burn(_minter, _usedLimit); 340 | vm.stopPrank(); 341 | 342 | vm.startPrank(_owner); 343 | // Removing 100k to the limit 344 | _xerc20.setLimits(_minter, _limit - 100_000, _limit - 100_000); 345 | vm.stopPrank(); 346 | 347 | assertEq(_xerc20.mintingCurrentLimitOf(_minter), (_limit - _usedLimit) - 100_000); 348 | assertEq(_xerc20.burningCurrentLimitOf(_minter), (_limit - _usedLimit) - 100_000); 349 | } 350 | 351 | function testChangingUsedLimitsToZero(uint256 _limit, uint256 _amount) public { 352 | _limit = bound(_limit, 1, 1e40); 353 | vm.assume(_amount < _limit); 354 | vm.startPrank(_owner); 355 | _xerc20.setLimits(_minter, _limit, _limit); 356 | vm.stopPrank(); 357 | 358 | vm.startPrank(_minter); 359 | _xerc20.mint(_minter, _amount); 360 | _xerc20.burn(_minter, _amount); 361 | vm.stopPrank(); 362 | 363 | vm.startPrank(_owner); 364 | _xerc20.setLimits(_minter, 0, 0); 365 | vm.stopPrank(); 366 | 367 | assertEq(_xerc20.mintingMaxLimitOf(_minter), 0); 368 | assertEq(_xerc20.mintingCurrentLimitOf(_minter), 0); 369 | assertEq(_xerc20.burningMaxLimitOf(_minter), 0); 370 | assertEq(_xerc20.burningCurrentLimitOf(_minter), 0); 371 | } 372 | 373 | function testCannotSetLockbox(address _lockbox) public { 374 | vm.prank(_owner); 375 | vm.expectRevert(); 376 | _xerc20.setLockbox(_lockbox); 377 | } 378 | 379 | function testRemoveBridge(uint256 _limit) public { 380 | _limit = bound(_limit, 1, type(uint256).max / 2); 381 | 382 | vm.startPrank(_owner); 383 | _xerc20.setLimits(_minter, _limit, _limit); 384 | 385 | assertEq(_xerc20.mintingMaxLimitOf(_minter), _limit); 386 | assertEq(_xerc20.burningMaxLimitOf(_minter), _limit); 387 | _xerc20.setLimits(_minter, 0, 0); 388 | vm.stopPrank(); 389 | 390 | assertEq(_xerc20.mintingMaxLimitOf(_minter), 0); 391 | assertEq(_xerc20.burningMaxLimitOf(_minter), 0); 392 | } 393 | 394 | function testStorageLayout() public { 395 | BridgedSuperTokenStorageTest storageTestContract = new BridgedSuperTokenStorageTest(); 396 | storageTestContract.validateStorageLayout(); 397 | } 398 | } 399 | 400 | contract BridgedSuperTokenStorageTest is BridgedSuperTokenProxy { 401 | error STORAGE_LOCATION_CHANGED(string _name); 402 | 403 | function validateStorageLayout() public pure { 404 | uint256 slot; 405 | uint256 offset; 406 | 407 | // storage slots 0-31: reserved for SuperToken logic via CustomSuperTokenBase 408 | // storage slot 32: Ownable | address _owner 409 | 410 | assembly { slot := bridges.slot offset := bridges.offset } 411 | if (slot != 33 || offset != 0) revert STORAGE_LOCATION_CHANGED("bridges"); 412 | } 413 | } -------------------------------------------------------------------------------- /test/xchain/OPBridgedSuperTokenTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.26; 3 | 4 | import { OPBridgedSuperTokenProxy, IOPBridgedSuperToken, IBridgedSuperToken, IOptimismMintableERC20 } from "../../src/xchain/OPBridgedSuperToken.sol"; 5 | import { BridgedSuperTokenTest } from "./BridgedSuperTokenTest.t.sol"; 6 | 7 | contract OPBridgedSuperTokenTest is BridgedSuperTokenTest { 8 | address internal _nativeBridge = address(99); 9 | address internal _remoteToken = address(98); 10 | IOPBridgedSuperToken internal _opToken; 11 | 12 | function _deployToken(address owner) internal override { 13 | // deploy proxy 14 | OPBridgedSuperTokenProxy proxy = new OPBridgedSuperTokenProxy(_nativeBridge, _remoteToken); 15 | // initialize proxy 16 | proxy.initialize(sf.superTokenFactory, "Test Token", "TT", _owner, 1000); 17 | proxy.transferOwnership(owner); 18 | 19 | _opToken = IOPBridgedSuperToken(address(proxy)); 20 | _xerc20 = IBridgedSuperToken(_opToken); 21 | } 22 | 23 | function testMintByNativeBridge(uint256 _amount) public { 24 | _amount = bound(_amount, 1, type(uint256).max / 2); 25 | 26 | vm.prank(_nativeBridge); 27 | _opToken.mint(_user, _amount); 28 | 29 | assertEq(_xerc20.balanceOf(_user), _amount); 30 | } 31 | 32 | function testBurnByNativeBridge(uint256 _amount) public { 33 | _amount = bound(_amount, 1, type(uint256).max / 2); 34 | 35 | vm.prank(_nativeBridge); 36 | _opToken.mint(_user, _amount); 37 | 38 | vm.prank(_user); 39 | _opToken.approve(_nativeBridge, _amount); 40 | 41 | vm.prank(_nativeBridge); 42 | _opToken.burn(_user, _amount); 43 | 44 | assertEq(_xerc20.balanceOf(_user), 0); 45 | } 46 | 47 | function testERC165InterfaceDetection() public view { 48 | assertTrue(_opToken.supportsInterface(type(IOptimismMintableERC20).interfaceId)); 49 | } 50 | } --------------------------------------------------------------------------------