├── .github └── workflows │ ├── CI.yaml │ └── Nightly.yaml ├── .gitignore ├── .nvmrc ├── .openzeppelin ├── .gitignore └── mainnet.json ├── .prettierrc ├── .solcover.js ├── .solhint.json ├── .yarn ├── install-state.gz ├── plugins │ └── @yarnpkg │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.2.1.cjs ├── .yarnrc.yml ├── INFO.md ├── LICENSE ├── README.md ├── contracts ├── Access │ ├── EIP712.sol │ ├── ERC1271.sol │ └── OwnableERC721.sol ├── ExclusiveGeyser.sol ├── Factory │ ├── GeyserRegistry.sol │ ├── IFactory.sol │ ├── InstanceRegistry.sol │ ├── PowerSwitchFactory.sol │ ├── ProxyFactory.sol │ ├── RewardPoolFactory.sol │ └── VaultFactory.sol ├── Geyser.sol ├── Mock │ ├── MockAmpl.sol │ ├── MockDelegate.sol │ ├── MockERC1271.sol │ ├── MockERC20.sol │ ├── MockGeyser.sol │ ├── MockPowered.sol │ ├── MockSmartWallet.sol │ ├── MockStakeHelper.sol │ └── MockVaultFactory.sol ├── PowerSwitch │ ├── PowerSwitch.sol │ └── Powered.sol ├── RewardPool.sol ├── Router │ ├── CharmGeyserRouter.sol │ └── GeyserRouter.sol └── UniversalVault.sol ├── frontend ├── .env.sample ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .yarn │ └── install-state.gz ├── LICENSE ├── README.md ├── craco.config.js ├── package.json ├── public │ ├── index.html │ ├── manifest.json │ └── robots.txt ├── scripts │ ├── deploy-dev.sh │ ├── deploy-prod.sh │ └── flush-cache-prod.sh ├── src │ ├── App.tsx │ ├── assets │ │ ├── caret_down.svg │ │ ├── checkmark_light.svg │ │ ├── clipboard.svg │ │ ├── geyser.webp │ │ ├── info.svg │ │ ├── rewardSymbol.svg │ │ ├── three.module.js │ │ ├── tokens │ │ │ ├── ampl.png │ │ │ ├── forth.png │ │ │ ├── spot.png │ │ │ ├── usdc.png │ │ │ ├── wampl.png │ │ │ ├── wbtc.png │ │ │ └── weth.png │ │ └── warning.svg │ ├── components │ │ ├── Body.tsx │ │ ├── Button.tsx │ │ ├── DisabledInput.tsx │ │ ├── Dropdown.tsx │ │ ├── DropdownsContainer.tsx │ │ ├── ErrorPage.tsx │ │ ├── EtherscanLink.tsx │ │ ├── Footer.tsx │ │ ├── FormLabel.tsx │ │ ├── GeyserFirst │ │ │ ├── ConnectWalletWarning.tsx │ │ │ ├── DepositInfoGraphic.tsx │ │ │ ├── EstimatedRewards.tsx │ │ │ ├── GeyserFirstContainer.tsx │ │ │ ├── GeyserInteractionButton.tsx │ │ │ ├── GeyserMultiStatsBox.tsx │ │ │ ├── GeyserStakeView.tsx │ │ │ ├── GeyserStats.tsx │ │ │ ├── GeyserStatsBox.tsx │ │ │ ├── GeyserStatsView.tsx │ │ │ ├── MyStats.tsx │ │ │ ├── MyStatsBox.tsx │ │ │ ├── StakeWarning.tsx │ │ │ ├── UnstakeConfirmModal.tsx │ │ │ ├── UnstakeSummary.tsx │ │ │ ├── UnstakeTxModal.tsx │ │ │ ├── UserBalance.tsx │ │ │ ├── WithdrawTxMessage.tsx │ │ │ ├── WrapperCheckbox.tsx │ │ │ └── WrapperWarning.tsx │ │ ├── GeysersList.tsx │ │ ├── Header.tsx │ │ ├── HeaderNetworkSelect.tsx │ │ ├── HeaderWalletButton.tsx │ │ ├── Home.tsx │ │ ├── Modal.tsx │ │ ├── PageLoader.css │ │ ├── PageLoader.tsx │ │ ├── PositiveInput.tsx │ │ ├── ProcessingButton.tsx │ │ ├── Select.tsx │ │ ├── SingleTxMessage.tsx │ │ ├── SingleTxModal.tsx │ │ ├── Spinner.tsx │ │ ├── TabView.tsx │ │ ├── Table.tsx │ │ ├── ThreeTabView.tsx │ │ ├── ToggleView.tsx │ │ ├── TokenIcons.tsx │ │ ├── ToolButton.tsx │ │ ├── Tooltip.tsx │ │ ├── TooltipTable.tsx │ │ ├── VaultFirst │ │ │ ├── VaultBalanceView.tsx │ │ │ └── VaultFirstContainer.tsx │ │ ├── VaultsList.tsx │ │ ├── WelcomeHero.tsx │ │ └── WelcomeMessage.tsx │ ├── config │ │ └── app.ts │ ├── constants.ts │ ├── context │ │ ├── AppContext.tsx │ │ ├── GeyserContext.tsx │ │ ├── StatsContext.tsx │ │ ├── SubgraphContext.tsx │ │ ├── VaultContext.tsx │ │ ├── WalletContext.tsx │ │ └── Web3Context.tsx │ ├── hooks │ │ └── useTxStateMachine.ts │ ├── index.css │ ├── index.tsx │ ├── queries │ │ ├── client.ts │ │ ├── geyser.ts │ │ └── vault.ts │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── sdk │ │ ├── abis.ts │ │ ├── actions.ts │ │ ├── deployments │ │ │ ├── avalanche │ │ │ │ └── factories-latest.json │ │ │ ├── goerli │ │ │ │ └── factories-latest.json │ │ │ ├── kovan │ │ │ │ └── factories-latest.json │ │ │ ├── localhost │ │ │ │ └── factories-latest.json │ │ │ └── mainnet │ │ │ │ └── factories-latest.json │ │ ├── index.ts │ │ ├── stats.ts │ │ ├── tokens.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── styling │ │ ├── colors.ts │ │ ├── mixins.ts │ │ └── styles.ts │ ├── types.ts │ └── utils │ │ ├── abis │ │ ├── AaveV2DepositToken.ts │ │ ├── ArrakisV1.ts │ │ ├── BalancerBPoolV1.ts │ │ ├── BalancerCRPV1.ts │ │ ├── BalancerVaultV2.ts │ │ ├── BalancerWeightedPoolV2.ts │ │ ├── BillBroker.ts │ │ ├── CharmV1.ts │ │ ├── MooniswapV1Pair.ts │ │ ├── SpotAppraiser.ts │ │ ├── Stampl.ts │ │ ├── UFragments.ts │ │ ├── UFragmentsPolicy.ts │ │ ├── UniswapV2Pair.ts │ │ ├── UniswapV3Pool.ts │ │ ├── WrappedERC20.ts │ │ ├── XCAmple.ts │ │ └── XCController.ts │ │ ├── amount.ts │ │ ├── ampleforth.ts │ │ ├── bonusToken.ts │ │ ├── cache.ts │ │ ├── eth.ts │ │ ├── formatDisplayAddress.ts │ │ ├── numeral.ts │ │ ├── price.ts │ │ ├── rewardToken.ts │ │ ├── stakingToken.ts │ │ ├── stats.ts │ │ ├── token.ts │ │ └── wrap.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock ├── hardhat.config.ts ├── package.json ├── sdk ├── subgraph ├── .gitignore ├── .prettierrc ├── .yarn │ └── install-state.gz ├── README.md ├── abis │ ├── IERC20.json │ ├── IERC721Enumerable.json │ ├── IGeyser.json │ ├── IInstanceRegistry.json │ ├── IPowerSwitch.json │ ├── IRebasingERC20.json │ └── IUniversalVault.json ├── configs │ ├── avalanche.json │ ├── kovan.json │ └── mainnet.json ├── package-lock.json ├── package.json ├── schema.graphql ├── scripts │ ├── deploy-local.sh │ └── deploy.sh ├── src │ ├── geyser.ts │ ├── rebasingToken.ts │ ├── utils.ts │ └── vault.ts ├── subgraph.template.yaml └── yarn.lock ├── test ├── Access │ └── ERC1271.ts ├── CharmGeyserRouter.ts ├── ExclusiveGeyser.ts ├── Geyser.ts ├── GeyserRouter.ts ├── PowerSwitch │ ├── PowerSwitch.ts │ └── Powered.ts ├── RewardPool.ts ├── UniversalVault.ts ├── VaultFactory.ts └── utils.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test-contracts: 11 | name: Test Contracts 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2-beta 17 | with: 18 | node-version: '20' 19 | - run: yarn install 20 | - run: yarn compile 21 | - run: yarn test -------------------------------------------------------------------------------- /.github/workflows/Nightly.yaml: -------------------------------------------------------------------------------- 1 | name: Nightly 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | test-contracts: 9 | name: Test Contracts 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2-beta 15 | with: 16 | node-version: '20' 17 | - run: yarn install 18 | - run: yarn compile 19 | - run: yarn test 20 | test-coverage: 21 | name: Test Contracts Coverage 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v2-beta 27 | with: 28 | node-version: '20' 29 | - run: yarn install 30 | - run: yarn compile 31 | - run: yarn coverage 32 | - uses: coverallsapp/github-action@v1.1.2 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | build 3 | dist 4 | 5 | ### Emacs ## 6 | *~ 7 | \#*\# 8 | .\#* 9 | 10 | # 11 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 12 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 13 | # 14 | 15 | # User-specific stuff: 16 | .idea/**/workspace.xml 17 | .idea/**/tasks.xml 18 | .idea/dictionaries 19 | 20 | # Sensitive or high-churn files: 21 | .idea/**/dataSources/ 22 | .idea/**/dataSources.ids 23 | .idea/**/dataSources.local.xml 24 | .idea/**/sqlDataSources.xml 25 | .idea/**/dynamic.xml 26 | .idea/**/uiDesigner.xml 27 | 28 | # Gradle: 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # CMake 33 | cmake-build-debug/ 34 | cmake-build-release/ 35 | 36 | # Mongo Explorer plugin: 37 | .idea/**/mongoSettings.xml 38 | 39 | ## File-based project format: 40 | *.iws 41 | 42 | ## Plugin-specific files: 43 | 44 | # IntelliJ 45 | out/ 46 | 47 | # mpeltonen/sbt-idea plugin 48 | .idea_modules/ 49 | 50 | # JIRA plugin 51 | atlassian-ide-plugin.xml 52 | 53 | # Cursive Clojure plugin 54 | .idea/replstate.xml 55 | 56 | # Crashlytics plugin (for Android Studio and IntelliJ) 57 | com_crashlytics_export_strings.xml 58 | crashlytics.properties 59 | crashlytics-build.properties 60 | fabric.properties 61 | 62 | # NodeJS dependencies 63 | node_modules/ 64 | 65 | # ES-Lint 66 | .eslintcache 67 | 68 | # Solidity-Coverage 69 | allFiredEvents 70 | scTopics 71 | scDebugLog 72 | coverage.json 73 | coverage/ 74 | coverageEnv/ 75 | 76 | .vscode 77 | cache 78 | artifacts 79 | generated 80 | 81 | # subgraph 82 | graph-node/ 83 | 84 | # env 85 | .env 86 | frontend/.env 87 | 88 | # rough notes 89 | notes.txt 90 | 91 | # flattened for verification 92 | flattened 93 | 94 | # user scripts 95 | scripts.sh 96 | 97 | # python 98 | .python-version 99 | 100 | # yarn 101 | .yarn/install-state.gz 102 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /.openzeppelin/.gitignore: -------------------------------------------------------------------------------- 1 | unknown*.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "bracketSpacing": true, 6 | "printWidth": 120, 7 | "overrides": [ 8 | { 9 | "files": "*.sol", 10 | "options": { 11 | "printWidth": 120, 12 | "tabWidth": 4, 13 | "useTabs": false, 14 | "singleQuote": false, 15 | "bracketSpacing": false, 16 | "explicitTypes": "always" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: ['Mock', 'Factory'], 3 | } 4 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "not-rely-on-time": "off", 7 | "max-line-length": ["error", 120] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/.yarn/install-state.gz -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nmHoistingLimits: workspaces 2 | 3 | nodeLinker: node-modules 4 | 5 | plugins: 6 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 7 | spec: "@yarnpkg/plugin-workspace-tools" 8 | 9 | yarnPath: .yarn/releases/yarn-3.2.1.cjs 10 | -------------------------------------------------------------------------------- /INFO.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Contract Changes 4 | 5 | - RouterV1 mints vaults, so it needs to be a `IERC721Receiver` and implement `onERC721Received`. 6 | - Added the `depositStake` contract function that does the same thing as the existing `create2VaultAndStake` function, except it doesn't create a vault. 7 | - Fixed a bug relating to the logic for when to update a partially unstaked stake in `unstakeAndClaim` of the Geyser contract. See lines 895-901 in `contracts/Geyser.sol` for a description of the change. 8 | 9 | ## Subgraph Changes 10 | 11 | Mainly bug fixes. Also added a `PowerSwitch` entity to keep track of the `GeyserStatus`, 12 | since the event is emitted by the `PowerSwitch` contract. 13 | 14 | # Local Deployment 15 | 16 | ## Deploy Local 17 | 18 | 1. `yarn hardhat node` -- this spins up a local hardhat network, it is useful to note down the accounts 19 | 2. `yarn hardhat compile` 20 | 3. `yarn hardhat deploy --mock --no-verify --network localhost` 21 | 4. `git clone https://github.com/graphprotocol/graph-node/` 22 | 5. `cd graph-node/docker` 23 | 6. If on Linux, `./setup.sh && docker-compose up`, otherwise `docker-compose up` 24 | 7. `cd subgraph`, `cat subgraph.template.yaml > subgraph.yaml` and update the addresses in `subgraph.yaml` (see [here](#addresses-of-data-sources-in-subgraph)) 25 | 8. `yarn && yarn codegen && yarn create-local && yarn deploy-local` 26 | 27 | ## Addresses of Data Sources in Subgraph 28 | 29 | The following is the mapping between the data source name to its corresponding contract. 30 | Consequently, the address of the data source should match the address of the deployed contract. 31 | 32 | Data Source | Contract 33 | ------------|----------- 34 | GeyserRegistry | GeyserRegistry 35 | VaultFactory | VaultFactory 36 | UniversalVaultNFT | VaultFactory 37 | CrucibleFactory | VaultFactory (of alchemist) 38 | CrucibleNFT | VaultFactory (of alchemist) 39 | 40 | # Additional Info 41 | 42 | Check out `frontend/README.md` for documentation relating to the frontend code. 43 | -------------------------------------------------------------------------------- /contracts/Access/ERC1271.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {ECDSA} from "@openzeppelin/contracts/cryptography/ECDSA.sol"; 5 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 6 | 7 | interface IERC1271 { 8 | function isValidSignature(bytes32 _messageHash, bytes memory _signature) external view returns (bytes4 magicValue); 9 | } 10 | 11 | library SignatureChecker { 12 | function isValidSignature( 13 | address signer, 14 | bytes32 hash, 15 | bytes memory signature 16 | ) internal view returns (bool) { 17 | if (Address.isContract(signer)) { 18 | bytes4 selector = IERC1271.isValidSignature.selector; 19 | (bool success, bytes memory returndata) = 20 | signer.staticcall(abi.encodeWithSelector(selector, hash, signature)); 21 | return success && abi.decode(returndata, (bytes4)) == selector; 22 | } else { 23 | return ECDSA.recover(hash, signature) == signer; 24 | } 25 | } 26 | } 27 | 28 | /// @title ERC1271 29 | /// @notice Module for ERC1271 compatibility 30 | /// @dev Security contact: dev-support@ampleforth.org 31 | abstract contract ERC1271 is IERC1271 { 32 | // Valid magic value bytes4(keccak256("isValidSignature(bytes32,bytes)") 33 | bytes4 internal constant VALID_SIG = IERC1271.isValidSignature.selector; 34 | // Invalid magic value 35 | bytes4 internal constant INVALID_SIG = bytes4(0); 36 | 37 | modifier onlyValidSignature(bytes32 permissionHash, bytes memory signature) { 38 | require(isValidSignature(permissionHash, signature) == VALID_SIG, "ERC1271: Invalid signature"); 39 | _; 40 | } 41 | 42 | function _getOwner() internal view virtual returns (address owner); 43 | 44 | function isValidSignature(bytes32 permissionHash, bytes memory signature) public view override returns (bytes4) { 45 | return SignatureChecker.isValidSignature(_getOwner(), permissionHash, signature) ? VALID_SIG : INVALID_SIG; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/Access/OwnableERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 5 | 6 | /// @title OwnableERC721 7 | /// @notice Use ERC721 ownership for access control 8 | /// @dev Security contact: dev-support@ampleforth.org 9 | contract OwnableERC721 { 10 | address private _nftAddress; 11 | 12 | modifier onlyOwner() { 13 | require(owner() == msg.sender, "OwnableERC721: caller is not the owner"); 14 | _; 15 | } 16 | 17 | function _setNFT(address nftAddress) internal { 18 | _nftAddress = nftAddress; 19 | } 20 | 21 | function nft() public view virtual returns (address nftAddress) { 22 | return _nftAddress; 23 | } 24 | 25 | function owner() public view virtual returns (address ownerAddress) { 26 | return IERC721(_nftAddress).ownerOf(uint256(address(this))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/ExclusiveGeyser.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import {IUniversalVault} from "./UniversalVault.sol"; 7 | import {Geyser} from "./Geyser.sol"; 8 | 9 | /// @title ExclusiveGeyser 10 | /// @notice A special extension of GeyserV2 which enforces that, 11 | /// no staking token balance may be staked in more than one geyser at a time. 12 | /// @dev Security contact: dev-support@ampleforth.org 13 | contract ExclusiveGeyser is Geyser { 14 | /// @inheritdoc Geyser 15 | function stake( 16 | address vault, 17 | uint256 amount, 18 | bytes calldata permission 19 | ) public override { 20 | // verify that vault isn't staking the same tokens in multiple programs 21 | _enforceExclusiveStake(IUniversalVault(vault), amount); 22 | 23 | // continue with regular stake 24 | super.stake(vault, amount, permission); 25 | } 26 | 27 | /// @dev Enforces that the vault can't use tokens which have already been staked. 28 | function _enforceExclusiveStake(IUniversalVault vault, uint256 amount) private view { 29 | require(amount <= computeAvailableStakingBalance(vault), "ExclusiveGeyser: expected exclusive stake"); 30 | } 31 | 32 | /// @notice Computes the amount of staking tokens in the vault available to be staked exclusively. 33 | function computeAvailableStakingBalance(IUniversalVault vault) public view returns (uint256) { 34 | // Iterates through the vault's locks to compute the total "stakingToken" balance staked across all geysers. 35 | address stakingToken = super.getGeyserData().stakingToken; 36 | uint256 vaultBal = IERC20(stakingToken).balanceOf(address(vault)); 37 | uint256 totalStakedBal = 0; 38 | uint256 lockCount = vault.getLockSetCount(); 39 | for (uint256 i = 0; i < lockCount; i++) { 40 | IUniversalVault.LockData memory lock = vault.getLockAt(i); 41 | if (lock.token == stakingToken) { 42 | totalStakedBal += lock.balance; 43 | } 44 | } 45 | return (vaultBal > totalStakedBal) ? vaultBal - totalStakedBal : 0; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/Factory/GeyserRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | import {InstanceRegistry} from "./InstanceRegistry.sol"; 7 | 8 | /// @title GeyserRegistry 9 | /// @dev Security contact: dev-support@ampleforth.org 10 | contract GeyserRegistry is InstanceRegistry, Ownable { 11 | function register(address instance) external onlyOwner { 12 | InstanceRegistry._register(instance); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/Factory/IFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | interface IFactory { 5 | function create(bytes calldata args) external returns (address instance); 6 | 7 | function create2(bytes calldata args, bytes32 salt) external returns (address instance); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/Factory/InstanceRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {EnumerableSet} from "@openzeppelin/contracts/utils/EnumerableSet.sol"; 5 | 6 | interface IInstanceRegistry { 7 | /* events */ 8 | 9 | event InstanceAdded(address instance); 10 | event InstanceRemoved(address instance); 11 | 12 | /* view functions */ 13 | 14 | function isInstance(address instance) external view returns (bool validity); 15 | 16 | function instanceCount() external view returns (uint256 count); 17 | 18 | function instanceAt(uint256 index) external view returns (address instance); 19 | } 20 | 21 | /// @title InstanceRegistry 22 | /// @dev Security contact: dev-support@ampleforth.org 23 | contract InstanceRegistry is IInstanceRegistry { 24 | using EnumerableSet for EnumerableSet.AddressSet; 25 | 26 | /* storage */ 27 | 28 | EnumerableSet.AddressSet private _instanceSet; 29 | 30 | /* view functions */ 31 | 32 | function isInstance(address instance) external view override returns (bool validity) { 33 | return _instanceSet.contains(instance); 34 | } 35 | 36 | function instanceCount() external view override returns (uint256 count) { 37 | return _instanceSet.length(); 38 | } 39 | 40 | function instanceAt(uint256 index) external view override returns (address instance) { 41 | return _instanceSet.at(index); 42 | } 43 | 44 | /* admin functions */ 45 | 46 | function _register(address instance) internal { 47 | require(_instanceSet.add(instance), "InstanceRegistry: already registered"); 48 | emit InstanceAdded(instance); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /contracts/Factory/PowerSwitchFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {EnumerableSet} from "@openzeppelin/contracts/utils/EnumerableSet.sol"; 5 | 6 | import {IFactory} from "./IFactory.sol"; 7 | import {InstanceRegistry} from "./InstanceRegistry.sol"; 8 | import {PowerSwitch} from "../PowerSwitch/PowerSwitch.sol"; 9 | 10 | /// @title Power Switch Factory 11 | /// @dev Security contact: dev-support@ampleforth.org 12 | contract PowerSwitchFactory is IFactory, InstanceRegistry { 13 | function create(bytes calldata args) external override returns (address) { 14 | address owner = abi.decode(args, (address)); 15 | PowerSwitch powerSwitch = new PowerSwitch(owner); 16 | InstanceRegistry._register(address(powerSwitch)); 17 | return address(powerSwitch); 18 | } 19 | 20 | function create2(bytes calldata, bytes32) external pure override returns (address) { 21 | revert("PowerSwitchFactory: unused function"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/Factory/ProxyFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | 4 | import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; 5 | 6 | library ProxyFactory { 7 | /* functions */ 8 | 9 | function _create(address logic, bytes memory data) internal returns (address proxy) { 10 | // deploy clone 11 | proxy = Clones.clone(logic); 12 | 13 | // attempt initialization 14 | if (data.length > 0) { 15 | (bool success, bytes memory err) = proxy.call(data); 16 | require(success, string(err)); 17 | } 18 | 19 | // explicit return 20 | return proxy; 21 | } 22 | 23 | function _create2( 24 | address logic, 25 | bytes memory data, 26 | bytes32 salt 27 | ) internal returns (address proxy) { 28 | // deploy clone 29 | proxy = Clones.cloneDeterministic(logic, salt); 30 | 31 | // attempt initialization 32 | if (data.length > 0) { 33 | (bool success, bytes memory err) = proxy.call(data); 34 | require(success, string(err)); 35 | } 36 | 37 | // explicit return 38 | return proxy; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/Factory/RewardPoolFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {EnumerableSet} from "@openzeppelin/contracts/utils/EnumerableSet.sol"; 5 | 6 | import {IFactory} from "./IFactory.sol"; 7 | import {InstanceRegistry} from "./InstanceRegistry.sol"; 8 | import {RewardPool} from "../RewardPool.sol"; 9 | 10 | /// @title Reward Pool Factory 11 | /// @dev Security contact: dev-support@ampleforth.org 12 | contract RewardPoolFactory is IFactory, InstanceRegistry { 13 | function create(bytes calldata args) external override returns (address) { 14 | address powerSwitch = abi.decode(args, (address)); 15 | RewardPool pool = new RewardPool(powerSwitch); 16 | InstanceRegistry._register(address(pool)); 17 | pool.transferOwnership(msg.sender); 18 | return address(pool); 19 | } 20 | 21 | function create2(bytes calldata, bytes32) external pure override returns (address) { 22 | revert("RewardPoolFactory: unused function"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/Factory/VaultFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; 6 | import {IFactory} from "./IFactory.sol"; 7 | import {IInstanceRegistry} from "./InstanceRegistry.sol"; 8 | import {IUniversalVault} from "../UniversalVault.sol"; 9 | import {ProxyFactory} from "./ProxyFactory.sol"; 10 | 11 | /// @title Vault Factory 12 | /// @dev Security contact: dev-support@ampleforth.org 13 | contract VaultFactory is IFactory, IInstanceRegistry, ERC721 { 14 | address private immutable _template; 15 | 16 | constructor(address template) ERC721("Universal Vault v1", "VAULT-v1") { 17 | require(template != address(0), "VaultFactory: invalid template"); 18 | _template = template; 19 | } 20 | 21 | /* registry functions */ 22 | 23 | function isInstance(address instance) external view override returns (bool validity) { 24 | return ERC721._exists(uint256(instance)); 25 | } 26 | 27 | function instanceCount() external view override returns (uint256 count) { 28 | return ERC721.totalSupply(); 29 | } 30 | 31 | function instanceAt(uint256 index) external view override returns (address instance) { 32 | return address(ERC721.tokenByIndex(index)); 33 | } 34 | 35 | /* factory functions */ 36 | 37 | function create(bytes calldata) external override returns (address vault) { 38 | return create(); 39 | } 40 | 41 | function create2(bytes calldata, bytes32 salt) external override returns (address vault) { 42 | return create2(salt); 43 | } 44 | 45 | function create() public returns (address vault) { 46 | // create clone and initialize 47 | vault = ProxyFactory._create(_template, abi.encodeWithSelector(IUniversalVault.initialize.selector)); 48 | 49 | // mint nft to caller 50 | ERC721._safeMint(msg.sender, uint256(vault)); 51 | 52 | // emit event 53 | emit InstanceAdded(vault); 54 | 55 | // explicit return 56 | return vault; 57 | } 58 | 59 | function create2(bytes32 salt) public returns (address vault) { 60 | // create clone and initialize 61 | vault = ProxyFactory._create2(_template, abi.encodeWithSelector(IUniversalVault.initialize.selector), salt); 62 | 63 | // mint nft to caller 64 | ERC721._safeMint(msg.sender, uint256(vault)); 65 | 66 | // emit event 67 | emit InstanceAdded(vault); 68 | 69 | // explicit return 70 | return vault; 71 | } 72 | 73 | /* getter functions */ 74 | 75 | function getTemplate() external view returns (address template) { 76 | return _template; 77 | } 78 | 79 | function predictCreate2Address(bytes32 salt) external view returns (address instance) { 80 | return Clones.predictDeterministicAddress(_template, salt, address(this)); 81 | } 82 | 83 | function addressToUint(address vault) external pure returns (uint256 tokenId) { 84 | return uint256(vault); 85 | } 86 | 87 | function uint256ToAddress(uint256 tokenId) external pure returns (address vault) { 88 | return address(tokenId); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/Mock/MockDelegate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {IRageQuit, IUniversalVault} from "../UniversalVault.sol"; 5 | 6 | contract MockDelegate is IRageQuit { 7 | enum DelegateType {Succeed, Revert, RevertWithMessage, OOG} 8 | 9 | DelegateType private _delegateType; 10 | 11 | function setDelegateType(DelegateType delegateType) external { 12 | _delegateType = delegateType; 13 | } 14 | 15 | function rageQuit() external view override { 16 | if (_delegateType == DelegateType.Succeed) { 17 | return; 18 | } else if (_delegateType == DelegateType.Revert) { 19 | revert(); 20 | } else if (_delegateType == DelegateType.RevertWithMessage) { 21 | require(false, "MockDelegate: revert with message"); 22 | } else if (_delegateType == DelegateType.OOG) { 23 | while (true) {} 24 | } 25 | } 26 | 27 | function lock( 28 | address vault, 29 | address token, 30 | uint256 amount, 31 | bytes memory permission 32 | ) external { 33 | IUniversalVault(vault).lock(token, amount, permission); 34 | } 35 | 36 | function unlock( 37 | address vault, 38 | address token, 39 | uint256 amount, 40 | bytes memory permission 41 | ) external { 42 | IUniversalVault(vault).unlock(token, amount, permission); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /contracts/Mock/MockERC1271.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {ERC1271} from "../Access/ERC1271.sol"; 5 | 6 | contract MockERC1271 is ERC1271 { 7 | address public owner; 8 | 9 | constructor(address _owner) { 10 | owner = _owner; 11 | } 12 | 13 | function _getOwner() internal view override returns (address) { 14 | return owner; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /contracts/Mock/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | import "@openzeppelin/contracts/drafts/IERC20Permit.sol"; 9 | 10 | contract MockERC20 is ERC20, IERC20Permit { 11 | mapping(address => uint256) public override nonces; 12 | 13 | constructor(address recipient, uint256 amount) ERC20("MockERC20", "MockERC20") { 14 | ERC20._mint(recipient, amount); 15 | } 16 | 17 | // Hard-coded DOMAIN_SEPARATOR for testing purposes only. 18 | // In a real implementation, this would be derived from EIP-712 parameters. 19 | function DOMAIN_SEPARATOR() external pure override returns (bytes32) { 20 | return 0x00; 21 | } 22 | 23 | function permit( 24 | address owner, 25 | address spender, 26 | uint256 value, 27 | uint256, // deadline - ignored in mock 28 | uint8, // v - ignored in mock 29 | bytes32, // r - ignored in mock 30 | bytes32 // s - ignored in mock 31 | ) external override { 32 | // For testing, ignore signature checks and deadline. 33 | // Simply set allowance directly. 34 | _approve(owner, spender, value); 35 | // Increment nonce as if a successful permit was used. 36 | nonces[owner]++; 37 | } 38 | } 39 | 40 | contract MockBAL is ERC20 { 41 | constructor(address recipient, uint256 amount) ERC20("MockBAL", "BAL") { 42 | ERC20._mint(recipient, amount); 43 | } 44 | } 45 | 46 | contract MockCharmLiqToken is ERC20 { 47 | address public token0; 48 | address public token1; 49 | 50 | constructor(address _token0, address _token1) ERC20("MockCharmLP", "MockCharmLP") { 51 | token0 = _token0; 52 | token1 = _token1; 53 | } 54 | 55 | function deposit( 56 | uint256 amount0, 57 | uint256 amount1, 58 | uint256 minAmount0, 59 | uint256 minAmount1, 60 | address to 61 | ) 62 | external 63 | returns ( 64 | uint256 lpAmt, 65 | uint256 actual0, 66 | uint256 actual1 67 | ) 68 | { 69 | require(amount0 >= minAmount0, "Insufficient token0"); 70 | require(amount1 >= minAmount1, "Insufficient token1"); 71 | lpAmt = amount0 + amount1; 72 | _mint(to, lpAmt); 73 | actual0 = amount0; 74 | actual1 = amount1; 75 | IERC20(token0).transferFrom(msg.sender, address(this), actual0); 76 | IERC20(token1).transferFrom(msg.sender, address(this), actual1); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /contracts/Mock/MockGeyser.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import {IGeyser} from "../Geyser.sol"; 6 | 7 | contract MockGeyser { 8 | address public stakingToken; 9 | 10 | constructor(address _stakingToken) { 11 | stakingToken = _stakingToken; 12 | } 13 | 14 | function getGeyserData() external view returns (IGeyser.GeyserData memory) { 15 | IGeyser.RewardSchedule[] memory rewardSchedules; 16 | return 17 | IGeyser.GeyserData({ 18 | stakingToken: stakingToken, 19 | rewardToken: address(0), 20 | rewardPool: address(0), 21 | rewardScaling: IGeyser.RewardScaling({floor: 0, ceiling: 0, time: 0}), 22 | rewardSharesOutstanding: 0, 23 | totalStake: 0, 24 | totalStakeUnits: 0, 25 | lastUpdate: 0, 26 | rewardSchedules: rewardSchedules 27 | }); 28 | } 29 | 30 | event Staked(address vault, uint256 amount, bytes permission); 31 | event UnstakedAndClaimed(address vault, uint256 amount, bytes permission); 32 | 33 | function stake( 34 | address vault, 35 | uint256 amount, 36 | bytes calldata permission 37 | ) external { 38 | emit Staked(vault, amount, permission); 39 | } 40 | 41 | function unstakeAndClaim( 42 | address vault, 43 | uint256 amount, 44 | bytes calldata permission 45 | ) external { 46 | emit UnstakedAndClaimed(vault, amount, permission); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /contracts/Mock/MockPowered.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {Powered} from "../PowerSwitch/Powered.sol"; 5 | 6 | contract MockPowered is Powered { 7 | constructor(address powerSwitch) { 8 | Powered._setPowerSwitch(powerSwitch); 9 | } 10 | 11 | function onlyOnlineCall() public view onlyOnline { 12 | return; 13 | } 14 | 15 | function onlyOfflineCall() public view onlyOffline { 16 | return; 17 | } 18 | 19 | function notShutdownCall() public view notShutdown { 20 | return; 21 | } 22 | 23 | function onlyShutdownCall() public view onlyShutdown { 24 | return; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/Mock/MockSmartWallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {ERC1271} from "../Access/ERC1271.sol"; 5 | 6 | contract MockSmartWallet is ERC1271 { 7 | address private _owner; 8 | 9 | constructor(address owner) { 10 | _owner = owner; 11 | } 12 | 13 | function _getOwner() internal view override returns (address) { 14 | return _owner; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /contracts/Mock/MockStakeHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 7 | import {IGeyser} from "../Geyser.sol"; 8 | import {IFactory} from "../Factory/IFactory.sol"; 9 | import {IUniversalVault} from "../UniversalVault.sol"; 10 | 11 | contract MockStakeHelper { 12 | function flashStake( 13 | address geyser, 14 | address vault, 15 | uint256 amount, 16 | bytes calldata lockPermission, 17 | bytes calldata unstakePermission 18 | ) external { 19 | IGeyser(geyser).stake(vault, amount, lockPermission); 20 | IGeyser(geyser).unstakeAndClaim(vault, amount, unstakePermission); 21 | } 22 | 23 | function stakeBatch( 24 | address[] calldata geysers, 25 | address[] calldata vaults, 26 | uint256[] calldata amounts, 27 | bytes[] calldata permissions 28 | ) external { 29 | for (uint256 index = 0; index < vaults.length; index++) { 30 | IGeyser(geysers[index]).stake(vaults[index], amounts[index], permissions[index]); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/Mock/MockVaultFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | 7 | contract MockVaultFactory is ERC721("MockVaultFactory", "MVF") { 8 | uint256 public nextVaultId; 9 | 10 | constructor() { 11 | nextVaultId = 1; 12 | } 13 | 14 | function create2( 15 | bytes calldata, /*args*/ 16 | bytes32 /*salt*/ 17 | ) external returns (address) { 18 | uint256 vaultId = nextVaultId; 19 | nextVaultId++; 20 | ERC721._safeMint(msg.sender, vaultId); 21 | return address(uint160(vaultId)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/PowerSwitch/PowerSwitch.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | interface IPowerSwitch { 7 | /* admin events */ 8 | 9 | event PowerOn(); 10 | event PowerOff(); 11 | event EmergencyShutdown(); 12 | 13 | /* data types */ 14 | 15 | enum State {Online, Offline, Shutdown} 16 | 17 | /* admin functions */ 18 | 19 | function powerOn() external; 20 | 21 | function powerOff() external; 22 | 23 | function emergencyShutdown() external; 24 | 25 | /* view functions */ 26 | 27 | function isOnline() external view returns (bool status); 28 | 29 | function isOffline() external view returns (bool status); 30 | 31 | function isShutdown() external view returns (bool status); 32 | 33 | function getStatus() external view returns (State status); 34 | 35 | function getPowerController() external view returns (address controller); 36 | } 37 | 38 | /// @title PowerSwitch 39 | /// @notice Standalone pausing and emergency stop functionality 40 | /// @dev Security contact: dev-support@ampleforth.org 41 | contract PowerSwitch is IPowerSwitch, Ownable { 42 | /* storage */ 43 | 44 | IPowerSwitch.State private _status; 45 | 46 | /* initializer */ 47 | 48 | constructor(address owner) { 49 | // sanity check owner 50 | require(owner != address(0), "PowerSwitch: invalid owner"); 51 | // transfer ownership 52 | Ownable.transferOwnership(owner); 53 | } 54 | 55 | /* admin functions */ 56 | 57 | /// @notice Turn Power On 58 | /// access control: only admin 59 | /// state machine: only when offline 60 | /// state scope: only modify _status 61 | /// token transfer: none 62 | function powerOn() external override onlyOwner { 63 | require(_status == IPowerSwitch.State.Offline, "PowerSwitch: cannot power on"); 64 | _status = IPowerSwitch.State.Online; 65 | emit PowerOn(); 66 | } 67 | 68 | /// @notice Turn Power Off 69 | /// access control: only admin 70 | /// state machine: only when online 71 | /// state scope: only modify _status 72 | /// token transfer: none 73 | function powerOff() external override onlyOwner { 74 | require(_status == IPowerSwitch.State.Online, "PowerSwitch: cannot power off"); 75 | _status = IPowerSwitch.State.Offline; 76 | emit PowerOff(); 77 | } 78 | 79 | /// @notice Shutdown Permanently 80 | /// access control: only admin 81 | /// state machine: 82 | /// - when online or offline 83 | /// - can only be called once 84 | /// state scope: only modify _status 85 | /// token transfer: none 86 | function emergencyShutdown() external override onlyOwner { 87 | require(_status != IPowerSwitch.State.Shutdown, "PowerSwitch: cannot shutdown"); 88 | _status = IPowerSwitch.State.Shutdown; 89 | emit EmergencyShutdown(); 90 | } 91 | 92 | /* getter functions */ 93 | 94 | function isOnline() external view override returns (bool status) { 95 | return _status == State.Online; 96 | } 97 | 98 | function isOffline() external view override returns (bool status) { 99 | return _status == State.Offline; 100 | } 101 | 102 | function isShutdown() external view override returns (bool status) { 103 | return _status == State.Shutdown; 104 | } 105 | 106 | function getStatus() external view override returns (IPowerSwitch.State status) { 107 | return _status; 108 | } 109 | 110 | function getPowerController() external view override returns (address controller) { 111 | return Ownable.owner(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /contracts/PowerSwitch/Powered.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {IPowerSwitch} from "./PowerSwitch.sol"; 5 | 6 | interface IPowered { 7 | function isOnline() external view returns (bool status); 8 | 9 | function isOffline() external view returns (bool status); 10 | 11 | function isShutdown() external view returns (bool status); 12 | 13 | function getPowerSwitch() external view returns (address powerSwitch); 14 | 15 | function getPowerController() external view returns (address controller); 16 | } 17 | 18 | /// @title Powered 19 | /// @notice Helper for calling external PowerSwitch 20 | /// @dev Security contact: dev-support@ampleforth.org 21 | contract Powered is IPowered { 22 | /* storage */ 23 | 24 | address private _powerSwitch; 25 | 26 | /* modifiers */ 27 | 28 | modifier onlyOnline() { 29 | _onlyOnline(); 30 | _; 31 | } 32 | 33 | modifier onlyOffline() { 34 | _onlyOffline(); 35 | _; 36 | } 37 | 38 | modifier notShutdown() { 39 | _notShutdown(); 40 | _; 41 | } 42 | 43 | modifier onlyShutdown() { 44 | _onlyShutdown(); 45 | _; 46 | } 47 | 48 | /* initializer */ 49 | 50 | function _setPowerSwitch(address powerSwitch) internal { 51 | _powerSwitch = powerSwitch; 52 | } 53 | 54 | /* getter functions */ 55 | 56 | function isOnline() public view override returns (bool status) { 57 | return IPowerSwitch(_powerSwitch).isOnline(); 58 | } 59 | 60 | function isOffline() public view override returns (bool status) { 61 | return IPowerSwitch(_powerSwitch).isOffline(); 62 | } 63 | 64 | function isShutdown() public view override returns (bool status) { 65 | return IPowerSwitch(_powerSwitch).isShutdown(); 66 | } 67 | 68 | function getPowerSwitch() public view override returns (address powerSwitch) { 69 | return _powerSwitch; 70 | } 71 | 72 | function getPowerController() public view override returns (address controller) { 73 | return IPowerSwitch(_powerSwitch).getPowerController(); 74 | } 75 | 76 | /* convenience functions */ 77 | 78 | function _onlyOnline() private view { 79 | require(isOnline(), "Powered: is not online"); 80 | } 81 | 82 | function _onlyOffline() private view { 83 | require(isOffline(), "Powered: is not offline"); 84 | } 85 | 86 | function _notShutdown() private view { 87 | require(!isShutdown(), "Powered: is shutdown"); 88 | } 89 | 90 | function _onlyShutdown() private view { 91 | require(isShutdown(), "Powered: is not shutdown"); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/RewardPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 6 | import {TransferHelper} from "@uniswap/lib/contracts/libraries/TransferHelper.sol"; 7 | 8 | import {Powered} from "./PowerSwitch/Powered.sol"; 9 | 10 | interface IRewardPool { 11 | function sendERC20( 12 | address token, 13 | address to, 14 | uint256 value 15 | ) external; 16 | 17 | function rescueERC20(address[] calldata tokens, address recipient) external; 18 | } 19 | 20 | /// @title Reward Pool 21 | /// @notice Vault for isolated storage of reward tokens 22 | /// @dev Security contact: dev-support@ampleforth.org 23 | contract RewardPool is IRewardPool, Powered, Ownable { 24 | /* initializer */ 25 | 26 | constructor(address powerSwitch) { 27 | Powered._setPowerSwitch(powerSwitch); 28 | } 29 | 30 | /* user functions */ 31 | 32 | /// @notice Send an ERC20 token 33 | /// access control: only owner 34 | /// state machine: 35 | /// - can be called multiple times 36 | /// - only online 37 | /// state scope: none 38 | /// token transfer: transfer tokens from self to recipient 39 | /// @param token address The token to send 40 | /// @param to address The recipient to send to 41 | /// @param value uint256 Amount of tokens to send 42 | function sendERC20( 43 | address token, 44 | address to, 45 | uint256 value 46 | ) external override onlyOwner onlyOnline { 47 | TransferHelper.safeTransfer(token, to, value); 48 | } 49 | 50 | /* emergency functions */ 51 | 52 | /// @notice Rescue multiple ERC20 tokens 53 | /// access control: only power controller 54 | /// state machine: 55 | /// - can be called multiple times 56 | /// - only shutdown 57 | /// state scope: none 58 | /// token transfer: transfer tokens from self to recipient 59 | /// @param tokens address[] The tokens to rescue 60 | /// @param recipient address The recipient to rescue to 61 | function rescueERC20(address[] calldata tokens, address recipient) external override onlyShutdown { 62 | // only callable by controller 63 | require(msg.sender == Powered.getPowerController(), "RewardPool: only controller can withdraw after shutdown"); 64 | 65 | // assert recipient is defined 66 | require(recipient != address(0), "RewardPool: recipient not defined"); 67 | 68 | // transfer tokens 69 | for (uint256 index = 0; index < tokens.length; index++) { 70 | // get token 71 | address token = tokens[index]; 72 | // get balance 73 | uint256 balance = IERC20(token).balanceOf(address(this)); 74 | // transfer token 75 | TransferHelper.safeTransfer(token, recipient, balance); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /contracts/Router/CharmGeyserRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import {GeyserRouter, TransferHelper, IERC20, IERC721, IGeyser} from "./GeyserRouter.sol"; 6 | 7 | /// @notice Interface for a Charm LP token 8 | interface ICharmLiqToken is IERC20 { 9 | function token0() external view returns (address); 10 | 11 | function token1() external view returns (address); 12 | 13 | function deposit( 14 | uint256, 15 | uint256, 16 | uint256, 17 | uint256, 18 | address 19 | ) 20 | external 21 | returns ( 22 | uint256, 23 | uint256, 24 | uint256 25 | ); 26 | } 27 | 28 | /// @title CharmGeyserRouter 29 | /// @notice Convenience contract to stake Charm LP tokens on geysers 30 | /// @dev Security contact: dev-support@ampleforth.org 31 | contract CharmGeyserRouter is GeyserRouter { 32 | using TransferHelper for address; 33 | 34 | struct LiqCreationPayload { 35 | uint256 token0Amt; 36 | uint256 token1Amt; 37 | uint256 token0MinAmt; 38 | uint256 token1MinAmt; 39 | } 40 | 41 | function createLiqAndStake( 42 | address geyser, 43 | address vault, 44 | bytes calldata permission, 45 | LiqCreationPayload memory d 46 | ) public { 47 | // Expects the geyser's staking token to be a Charm liquidity token 48 | ICharmLiqToken charm = ICharmLiqToken(IGeyser(geyser).getGeyserData().stakingToken); 49 | address depositToken0 = charm.token0(); 50 | address depositToken1 = charm.token1(); 51 | 52 | // Get deposit tokens from user 53 | depositToken0.safeTransferFrom(msg.sender, address(this), d.token0Amt); 54 | depositToken1.safeTransferFrom(msg.sender, address(this), d.token1Amt); 55 | 56 | // Creates a charm liquidity position and 57 | // transfers liquidity tokens directly to the vault 58 | _checkAndApproveMax(depositToken0, address(charm), d.token0Amt); 59 | _checkAndApproveMax(depositToken1, address(charm), d.token1Amt); 60 | (uint256 lpAmt, , ) = charm.deposit(d.token0Amt, d.token1Amt, d.token0MinAmt, d.token1MinAmt, vault); 61 | 62 | // Stake liquidity tokens from the vault 63 | IGeyser(geyser).stake(vault, lpAmt, permission); 64 | 65 | // Transfer any remaining dust deposit tokens 66 | _transferAll(depositToken0, msg.sender); 67 | _transferAll(depositToken1, msg.sender); 68 | } 69 | 70 | function create2VaultCreateLiqAndStake( 71 | address geyser, 72 | address vaultFactory, 73 | address vaultOwner, 74 | bytes32 salt, 75 | bytes calldata permission, 76 | LiqCreationPayload memory d 77 | ) external returns (address vault) { 78 | vault = create2Vault(vaultFactory, salt, vaultOwner); 79 | // create liquidity and stake 80 | createLiqAndStake(geyser, vault, permission, d); 81 | } 82 | 83 | /// @dev Checks if the spender has sufficient allowance. If not, approves the maximum possible amount. 84 | function _checkAndApproveMax( 85 | address token, 86 | address spender, 87 | uint256 amount 88 | ) private { 89 | uint256 allowance = IERC20(token).allowance(address(this), spender); 90 | if (allowance < amount) { 91 | IERC20(token).approve(spender, type(uint256).max); 92 | } 93 | } 94 | 95 | /// @dev Transfers the entire token balance to the given address. 96 | function _transferAll(address token, address to) private { 97 | token.safeTransfer(to, IERC20(token).balanceOf(address(this))); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /frontend/.env.sample: -------------------------------------------------------------------------------- 1 | ESLINT_NO_DEV_ERRORS=true 2 | SKIP_PREFLIGHT_CHECK=true 3 | ALCHEMY_PROJECT_ID= -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | src/assets/three.module.js -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'plugin:react/recommended', 8 | 'airbnb-typescript', 9 | 'prettier', // delegates formatting to prettiers 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 12, 17 | sourceType: 'module', 18 | project: './tsconfig.json', 19 | }, 20 | ignorePatterns: ['src/sdk'], 21 | plugins: ['react', '@typescript-eslint'], 22 | rules: { 23 | '@typescript-eslint/no-use-before-define': 'off', // define helper components at bottom of file 24 | '@typescript-eslint/no-unused-expressions': ['warn', { allowShortCircuit: true }], // allow the f && f() pattern, 25 | 'react/react-in-jsx-scope': 'off', 26 | 'react/prop-types': 'off', // using Typescript's type checking facilities instead 27 | 'react/require-default-props': 'off', // using ES6 default values instead 28 | 'react/jsx-props-no-spreading': 'off', // use this for convenience, 29 | 'import/no-extraneous-dependencies': 'off', // allow dependencies from sdk 30 | 'import/prefer-default-export': 'off', 31 | 'no-plusplus': ['warn', { allowForLoopAfterthoughts: true }], 32 | // TODO: enable before shipping 33 | 'jsx-a11y/label-has-associated-control': 'off', 34 | 'jsx-a11y/click-events-have-key-events': 'off', 35 | 'jsx-a11y/no-noninteractive-element-interactions': 'off', 36 | 'jsx-a11y/interactive-supports-focus': 'off', 37 | 'no-else-return': 'off', // seems to be buggy 38 | 'no-await-in-loop': 'off', 39 | 'guard-for-in': 'off', 40 | 'func-names': 'off', 41 | 'prefer-arrow-callback': 'off', 42 | }, 43 | settings: { 44 | 'import/resolver': { 45 | //need this and eslint-import-resolver-typescript for eslint to correctly resolve 46 | typescript: {}, 47 | }, 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env 26 | factories-*.json 27 | !factories-latest.json 28 | 29 | # yarn 30 | .yarn/install-state.gz 31 | -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules -------------------------------------------------------------------------------- /frontend/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/frontend/.yarn/install-state.gz -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hypotenuse Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/craco.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | style: { 5 | postcss: { 6 | plugins: [require('tailwindcss'), require('autoprefixer')], 7 | }, 8 | }, 9 | 10 | webpack: { 11 | configure: (webpackConfig) => { 12 | webpackConfig.module.rules.push({ 13 | test: /\.js$/, 14 | include: [ 15 | path.resolve(__dirname, 'node_modules/@web3-onboard/core'), 16 | path.resolve(__dirname, 'node_modules/@web3-onboard/injected-wallets'), 17 | path.resolve(__dirname, 'node_modules/@web3-onboard/wagmi'), 18 | path.resolve(__dirname, 'node_modules/@wagmi'), 19 | path.resolve(__dirname, 'node_modules/viem'), 20 | ], 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | presets: ['@babel/preset-env'], 25 | plugins: [ 26 | '@babel/plugin-proposal-numeric-separator', 27 | '@babel/plugin-proposal-nullish-coalescing-operator', 28 | '@babel/plugin-proposal-optional-chaining', 29 | ], 30 | }, 31 | }, 32 | }) 33 | return webpackConfig 34 | }, 35 | }, 36 | 37 | babel: { 38 | plugins: ['@babel/plugin-proposal-nullish-coalescing-operator', '@babel/plugin-proposal-optional-chaining'], 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ampleforthorg/sdk": "1.0.33", 7 | "@apollo/client": "^3.3.16", 8 | "@craco/craco": "^6.1.2", 9 | "@headlessui/react": "^1.7.18", 10 | "@heroicons/react": "v1", 11 | "@testing-library/jest-dom": "^5.11.4", 12 | "@testing-library/react": "^11.1.0", 13 | "@testing-library/user-event": "^12.1.10", 14 | "@types/jest": "^26.0.15", 15 | "@types/node": "^12.0.0", 16 | "@types/react": "^17.0.0", 17 | "@types/react-dom": "^17.0.0", 18 | "@types/styled-components": "^5.1.9", 19 | "@web3-onboard/coinbase": "^2.4.1", 20 | "@web3-onboard/core": "^2.23.0", 21 | "@web3-onboard/gas": "^2.2.1", 22 | "@web3-onboard/gnosis": "^2.3.1", 23 | "@web3-onboard/injected-wallets": "^2.11.2", 24 | "@web3-onboard/ledger": "^2.7.1", 25 | "@web3-onboard/metamask": "^2.1.1", 26 | "@web3-onboard/react": "^2.10.0", 27 | "@web3-onboard/wagmi": "^2.0.1", 28 | "@web3-onboard/walletconnect": "^2.6.1", 29 | "@web3-onboard/web3auth": "^2.3.1", 30 | "graphql": "^15.5.0", 31 | "numeral-es6": "^1.0.0", 32 | "react": "^17.0.2", 33 | "react-dom": "^17.0.2", 34 | "react-router-dom": "^6.6.0", 35 | "react-scripts": "4.0.3", 36 | "react-spring": "^9.2.3", 37 | "string_decoder": "^1.3.0", 38 | "styled-components": "^5.3.0", 39 | "twin.macro": "^2.4.2", 40 | "typescript": "^4.1.2", 41 | "web-vitals": "^1.0.1", 42 | "web3": "^1.3.5" 43 | }, 44 | "scripts": { 45 | "start": "craco start", 46 | "build": "craco build", 47 | "test": "craco test", 48 | "eject": "react-scripts eject", 49 | "lint:fix": "eslint src --ext .ts,.tsx --fix", 50 | "lint": "eslint src --ext .ts,.tsx", 51 | "format": "prettier --write ." 52 | }, 53 | "browserslist": { 54 | "production": [ 55 | ">0.2%", 56 | "not dead", 57 | "not op_mini all" 58 | ], 59 | "development": [ 60 | "last 1 chrome version", 61 | "last 1 firefox version", 62 | "last 1 safari version" 63 | ] 64 | }, 65 | "husky": { 66 | "hooks": { 67 | "pre-commit": "lint-staged" 68 | } 69 | }, 70 | "lint-staged": { 71 | "src/**/*.{js,jsx,ts,tsx,json,md}": [ 72 | "prettier --write" 73 | ] 74 | }, 75 | "babelMacros": { 76 | "twin": { 77 | "config": "./tailwind.config.js", 78 | "preset": "styled-components" 79 | } 80 | }, 81 | "devDependencies": { 82 | "@babel/plugin-proposal-numeric-separator": "^7.18.6", 83 | "@types/coingecko-api": "^1.0.0", 84 | "@typescript-eslint/eslint-plugin": "^4.26.0", 85 | "@typescript-eslint/parser": "^4.26.0", 86 | "autoprefixer": "^9.8.6", 87 | "eslint": "^7.27.0", 88 | "eslint-config-airbnb": "^18.2.1", 89 | "eslint-config-airbnb-typescript": "^12.3.1", 90 | "eslint-config-prettier": "^8.3.0", 91 | "eslint-import-resolver-typescript": "^2.4.0", 92 | "eslint-plugin-import": "^2.23.4", 93 | "eslint-plugin-jsx-a11y": "^6.4.1", 94 | "eslint-plugin-react": "^7.24.0", 95 | "eslint-plugin-react-hooks": "^4.2.0", 96 | "husky": "^6.0.0", 97 | "lint-staged": "^11.0.0", 98 | "postcss": "^7.0.35", 99 | "prettier": "^2.3.0", 100 | "react-is": "^17.0.2", 101 | "serve": "^14.2.4", 102 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.1.2" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Geyser 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Geyser 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 50 | 54 | 55 | 56 | 57 | 67 |
68 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Geyser", 3 | "short_name": "Geyser", 4 | "description": "Geysers are smart faucets that incentivize AMPL and SPOT on-chain liquidity.", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "background_color": "#000000", 8 | "theme_color": "#000000", 9 | "icons": [ 10 | { 11 | "src": "https://assets.fragments.org/ampl_favicon_32x32.png", 12 | "type": "image/png", 13 | "sizes": "32x32" 14 | }, 15 | { 16 | "src": "https://assets.fragments.org/ampl_favicon_32x32.png", 17 | "type": "image/png", 18 | "sizes": "48x48" 19 | }, 20 | { 21 | "src": "https://assets.fragments.org/ampl_favicon_32x32.png", 22 | "type": "image/png", 23 | "sizes": "72x72" 24 | }, 25 | { 26 | "src": "https://assets.fragments.org/ampl_favicon_32x32.png", 27 | "type": "image/png", 28 | "sizes": "96x96" 29 | }, 30 | { 31 | "src": "https://assets.fragments.org/ampl_favicon_32x32.png", 32 | "type": "image/png", 33 | "sizes": "144x144" 34 | }, 35 | { 36 | "src": "https://assets.fragments.org/ampl_favicon_32x32.png", 37 | "type": "image/png", 38 | "sizes": "192x192" 39 | }, 40 | { 41 | "src": "https://assets.fragments.org/ampl_favicon_32x32.png", 42 | "type": "image/png", 43 | "sizes": "512x512" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/scripts/deploy-dev.sh: -------------------------------------------------------------------------------- 1 | yarn build 2 | aws s3 sync build/ s3://dev.spot.cash/ --cache-control max-age=86400 -------------------------------------------------------------------------------- /frontend/scripts/deploy-prod.sh: -------------------------------------------------------------------------------- 1 | yarn build 2 | aws s3 sync build/ s3://geyser.ampleforth.org/ --cache-control max-age=86400 --acl public-read 3 | -------------------------------------------------------------------------------- /frontend/scripts/flush-cache-prod.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Name: flush-cache-prod.sh 4 | # 5 | # Usage: ./flush-cache-prod.sh "" 6 | # 7 | # Description: 8 | # 1. Looks up a CloudFront distribution by matching the input domain name 9 | # (e.g., "d123abcxyz.cloudfront.net") to the distribution's DomainName field. 10 | # 2. Issues a cache invalidation for all files ("/*"). 11 | # 3. Monitors the invalidation until it is completed. 12 | # 13 | # Requirements: 14 | # - AWS CLI installed and configured 15 | # - jq installed (for JSON parsing) 16 | # - A CloudFront distribution whose DomainName matches the supplied input 17 | 18 | set -euo pipefail 19 | 20 | if [ "$#" -ne 1 ]; then 21 | echo "Usage: $0 \"\"" 22 | echo "Example: $0 \"d123abcxyz.cloudfront.net\"" 23 | exit 1 24 | fi 25 | 26 | DISTRIBUTION_DOMAIN="$1" 27 | 28 | echo "Looking up the distribution by domain name: \"$DISTRIBUTION_DOMAIN\" ..." 29 | 30 | # Step 1: Query the distribution list, matching the DomainName to DISTRIBUTION_DOMAIN 31 | DISTRIBUTION_ID=$( 32 | aws cloudfront list-distributions --output json \ 33 | | jq -r --arg DOMAIN "$DISTRIBUTION_DOMAIN" ' 34 | .DistributionList.Items[] 35 | | select(.DomainName == $DOMAIN) 36 | | .Id 37 | ' 38 | ) 39 | 40 | if [ -z "$DISTRIBUTION_ID" ]; then 41 | echo "Error: No distribution found with domain name: \"$DISTRIBUTION_DOMAIN\"" 42 | exit 1 43 | fi 44 | 45 | echo "Found distribution ID: $DISTRIBUTION_ID" 46 | 47 | # Step 2: Issue a cache invalidation for all files 48 | echo "Creating invalidation for all paths (/*) ..." 49 | INVALIDATION_JSON=$( 50 | aws cloudfront create-invalidation \ 51 | --distribution-id "$DISTRIBUTION_ID" \ 52 | --paths "/*" 53 | ) 54 | 55 | # Extract the invalidation ID from the result 56 | INVALIDATION_ID=$(echo "$INVALIDATION_JSON" | jq -r '.Invalidation.Id') 57 | 58 | echo "Invalidation created. ID: $INVALIDATION_ID" 59 | 60 | # Step 3: Monitor the invalidation until it completes 61 | echo "Monitoring invalidation status..." 62 | 63 | while true; do 64 | STATUS=$( 65 | aws cloudfront get-invalidation \ 66 | --distribution-id "$DISTRIBUTION_ID" \ 67 | --id "$INVALIDATION_ID" \ 68 | --output json \ 69 | | jq -r '.Invalidation.Status' 70 | ) 71 | 72 | echo "Current status: $STATUS" 73 | 74 | if [ "$STATUS" == "Completed" ]; then 75 | echo "Invalidation $INVALIDATION_ID has completed!" 76 | break 77 | fi 78 | 79 | # Sleep for a few seconds before checking again (avoid spamming AWS) 80 | sleep 5 81 | done 82 | 83 | echo "Cache flush complete." 84 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' 2 | import { Header } from 'components/Header' 3 | import { Home } from 'components/Home' 4 | import { VaultContextProvider } from 'context/VaultContext' 5 | import { GeyserContextProvider } from 'context/GeyserContext' 6 | import { Web3Provider } from 'context/Web3Context' 7 | import { SubgraphProvider } from 'context/SubgraphContext' 8 | import { WalletContextProvider } from 'context/WalletContext' 9 | import { StatsContextProvider } from 'context/StatsContext' 10 | import { DropdownsContainer } from 'components/DropdownsContainer' 11 | 12 | import { VaultFirstContainer } from 'components/VaultFirst/VaultFirstContainer' 13 | import { GeyserFirstContainer } from 'components/GeyserFirst/GeyserFirstContainer' 14 | 15 | function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | } /> 27 | 31 | 32 | 33 | 34 | } 35 | /> 36 | 40 | 41 | 42 | 43 | } 44 | /> 45 | } /> 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | 57 | export default App 58 | -------------------------------------------------------------------------------- /frontend/src/assets/caret_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/checkmark_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/geyser.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/frontend/src/assets/geyser.webp -------------------------------------------------------------------------------- /frontend/src/assets/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/rewardSymbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/assets/tokens/ampl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/frontend/src/assets/tokens/ampl.png -------------------------------------------------------------------------------- /frontend/src/assets/tokens/forth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/frontend/src/assets/tokens/forth.png -------------------------------------------------------------------------------- /frontend/src/assets/tokens/spot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/frontend/src/assets/tokens/spot.png -------------------------------------------------------------------------------- /frontend/src/assets/tokens/usdc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/frontend/src/assets/tokens/usdc.png -------------------------------------------------------------------------------- /frontend/src/assets/tokens/wampl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/frontend/src/assets/tokens/wampl.png -------------------------------------------------------------------------------- /frontend/src/assets/tokens/wbtc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/frontend/src/assets/tokens/wbtc.png -------------------------------------------------------------------------------- /frontend/src/assets/tokens/weth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/frontend/src/assets/tokens/weth.png -------------------------------------------------------------------------------- /frontend/src/assets/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/components/Body.tsx: -------------------------------------------------------------------------------- 1 | import { AppContext } from 'context/AppContext' 2 | import { useContext } from 'react' 3 | import { GeyserFirstContainer } from './GeyserFirst/GeyserFirstContainer' 4 | import { VaultFirstContainer } from './VaultFirst/VaultFirstContainer' 5 | import { Mode } from '../constants' 6 | 7 | export const Body = () => { 8 | const { mode } = useContext(AppContext) 9 | return mode === Mode.VAULTS ? : 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from './Spinner' 2 | 3 | interface Props extends React.ButtonHTMLAttributes { 4 | isLoading?: boolean 5 | } 6 | 7 | export const Button: React.FC = (props) => { 8 | const { isLoading, children } = props 9 | return ( 10 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/DisabledInput.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | 4 | interface Props extends Omit, 'onChange'> { 5 | value: string 6 | } 7 | 8 | export const DisabledInput: React.FC = (props) => ( 9 | 10 | 11 | 12 | ) 13 | 14 | const Container = styled.div` 15 | ${tw`flex flex-row border border-gray h-fit mb-3 mt-1 rounded-md`} 16 | ` 17 | 18 | const Input = styled.input` 19 | ::-webkit-inner-spin-button { 20 | -webkit-appearance: none; 21 | margin: 0; 22 | } 23 | ::-webkit-outer-spin-button { 24 | -webkit-appearance: none; 25 | margin: 0; 26 | } 27 | ${tw`w-full tracking-wider rounded-lg p-3 text-base`} 28 | ${tw`focus:outline-none`} 29 | ` 30 | -------------------------------------------------------------------------------- /frontend/src/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { Listbox, Transition } from '@headlessui/react' 2 | import styled from 'styled-components/macro' 3 | import tw from 'twin.macro' 4 | import caretDown from 'assets/caret_down.svg' 5 | import checkMark from 'assets/checkmark_light.svg' 6 | import { Fragment } from 'react' 7 | 8 | // needs one of options or optgroups 9 | interface Props { 10 | options?: string[] 11 | optgroups?: { group: string; options: string[] }[] 12 | selectedOption: string 13 | onChange: (arg0: string) => void 14 | } 15 | 16 | export const Dropdown: React.FC = ({ options, optgroups, selectedOption, onChange }) => { 17 | const renderOptions = (opts: string[]) => 18 | opts.map((option) => ( 19 | 22 | `${active ? 'text-white bg-gray' : 'text-gray'} 23 | cursor-pointer hover:cursor-pointer select-none relative py-2 pl-10 pr-4 text-left` 24 | } 25 | value={option} 26 | > 27 | {({ selected }) => ( 28 | <> 29 | {option} 30 | {selected ? ( 31 | 32 | Selected 33 | 34 | ) : null} 35 | 36 | )} 37 | 38 | )) 39 | 40 | const renderOptgroups = (groups: { group: string; options: string[] }[]) => 41 | groups.map(({ group, options: opts }) => ( 42 |
43 | 48 | {group} 49 | 50 | {renderOptions(opts)} 51 |
52 | )) 53 | 54 | return ( 55 | 56 | 57 | 58 | {selectedOption} 59 | 60 | Down arrow 61 | 62 | 63 | 64 | 65 | {optgroups ? renderOptgroups(optgroups) : renderOptions(options || [])} 66 | 67 | 68 | 69 | 70 | ) 71 | } 72 | 73 | const Img = styled.img` 74 | ${tw`w-5 h-5 text-gray`} 75 | ` 76 | 77 | const OptionsWrapper = styled.div` 78 | ${tw`relative mt-1 w-336px`} 79 | ` 80 | -------------------------------------------------------------------------------- /frontend/src/components/DropdownsContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import styled from 'styled-components/macro' 3 | import tw from 'twin.macro' 4 | import Web3Context from 'context/Web3Context' 5 | import { VaultContext } from 'context/VaultContext' 6 | import { VaultsList } from './VaultsList' 7 | import { GeysersList } from './GeysersList' 8 | 9 | export const DropdownsContainer = ({ showGeysers, showVaults }) => { 10 | const { ready, validNetwork } = useContext(Web3Context) 11 | const { vaults } = useContext(VaultContext) 12 | if (validNetwork) { 13 | return ( 14 | 15 |
16 | {showVaults && ready && vaults.length > 0 ? : <>} 17 | {showGeysers ? : <>} 18 |
19 |
20 | ) 21 | } 22 | return <> 23 | } 24 | 25 | const Center = styled.div` 26 | ${tw`flex-row`} 27 | ${tw`text-center m-auto my-4 flex flex-col`} 28 | ` 29 | 30 | const Container = styled.div` 31 | ${tw`text-center m-auto my-4 flex flex-row w-full justify-center items-center`} 32 | ${tw`sm:w-sm`} 33 | ` 34 | -------------------------------------------------------------------------------- /frontend/src/components/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | import geyserImage from 'assets/geyser.webp' 4 | 5 | export const ErrorPage = ({ message, button, onClick }) => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {message} 15 | 16 | {button} 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | 24 | const Container = styled.div` 25 | ${tw`text-center m-auto my-4 flex flex-col flex-wrap w-full`} 26 | ${tw`sm:w-sm`} 27 | ` 28 | 29 | const ImageWrapper = styled.div` 30 | ${tw`flex justify-center my-4`} 31 | ` 32 | 33 | const GeyserImage = styled.img` 34 | ${tw`max-w-full w-full`} 35 | width:300px; 36 | ` 37 | 38 | const ErrorMessageContainer = styled.div` 39 | ${tw`h-80px mt-1 mb-5 border border-lightGray flex flex-row tracking-wider`} 40 | ` 41 | 42 | const ErrorColoredDiv = styled.div` 43 | ${tw`h-full w-2 bg-secondaryDark`} 44 | ` 45 | 46 | const ErrorContent = styled.div` 47 | ${tw`flex flex-row flex-grow text-white bg-secondary font-bold`} 48 | ` 49 | 50 | const ErrorMessageContainerInner = styled.div` 51 | ${tw`flex flex-row flex-grow w-full justify-between items-center px-5`} 52 | ` 53 | 54 | const ErrorMessage = styled.span` 55 | ${tw`my-auto`} 56 | ` 57 | 58 | const ButtonWrapper = styled.div` 59 | ${tw`flex-shrink-0`} 60 | ` 61 | 62 | const BackButton = styled.button` 63 | ${tw`uppercase font-bold bg-secondaryDark text-white w-120px h-40px rounded`} 64 | ${tw`sm:text-sm`} 65 | ${tw`hover:border hover:border-white cursor-pointer`} 66 | ` 67 | -------------------------------------------------------------------------------- /frontend/src/components/EtherscanLink.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import Web3Context from 'context/Web3Context' 3 | import { getConnectionConfig } from 'config/app' 4 | 5 | interface Props { 6 | txHash?: string 7 | } 8 | 9 | export const EtherscanLink: React.FC = ({ txHash }) => { 10 | const { networkId } = useContext(Web3Context) 11 | const { explorerUrl } = getConnectionConfig(networkId) 12 | return ( 13 | 14 | Block explorer 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | 4 | export const Footer = () => ( 5 | 6 | 7 | 8 | 9 | 13 | Getting started 14 | 15 | {' | '} 16 | 20 | Mining Rewards 21 | 22 | {' | '} 23 | 24 | GeyserV1 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | 32 | const Container = styled.div` 33 | ${tw`shadow-sm flex flex-wrap py-1 -mt-1 h-fit`} 34 | ` 35 | 36 | const LeftContainer = styled.div` 37 | ${tw`flex w-auto`} 38 | ${tw`w-2/12`} 39 | ` 40 | 41 | const MiddleContainer = styled.div` 42 | ${tw`flex flex-col xl:flex-row items-center justify-center w-full order-3 py-6`} 43 | ${tw`py-0 max-w-830px mx-auto order-2 w-1/3 xl:w-7/12`} 44 | ` 45 | 46 | const RightContainer = styled.div` 47 | ${tw`ml-auto order-2 w-auto`} 48 | ${tw`ml-0 order-3 w-2/12`} 49 | ` 50 | 51 | // NOTE: this is hot fix! 52 | // Remove !important 53 | const LinkSpan = styled.span` 54 | ${tw`ml-2 p-3`} 55 | ${tw`ml-20`} 56 | margin:0px !important; 57 | ` 58 | -------------------------------------------------------------------------------- /frontend/src/components/FormLabel.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | 4 | interface Props { 5 | text: string 6 | } 7 | 8 | export const FormLabel: React.FC = ({ text }) => ( 9 | 10 | 11 | {text}: 12 | 13 | 14 | ) 15 | 16 | const Text = styled.span` 17 | ${tw`text-xs sm:text-sm`} 18 | ` 19 | 20 | const FlexDiv = styled.div` 21 | ${tw`flex`} 22 | ` 23 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/ConnectWalletWarning.tsx: -------------------------------------------------------------------------------- 1 | import tw from 'twin.macro' 2 | import styled from 'styled-components/macro' 3 | 4 | interface Props { 5 | onClick: () => void 6 | } 7 | 8 | export const ConnectWalletWarning: React.FC = ({ onClick }) => ( 9 | 10 | 11 | 12 | 13 | Connect Your Ethereum Wallet 14 | 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | 22 | const Content = styled.div` 23 | ${tw`flex flex-row flex-grow text-white bg-secondary font-bold`} 24 | ` 25 | 26 | const ConnectWalletWarningContainer = styled.div` 27 | ${tw`h-80px mt-1 mb-5 border border-lightGray flex flex-row tracking-wider`} 28 | ` 29 | 30 | const ColoredDiv = styled.div` 31 | ${tw`h-full w-2 bg-secondaryDark`} 32 | ` 33 | 34 | const Message = styled.span` 35 | ${tw`ml-5 my-auto`} 36 | ` 37 | 38 | const ButtonWrapper = styled.div` 39 | ${tw`flex-grow w-2/12`} 40 | ` 41 | 42 | const Button = styled.button` 43 | ${tw`uppercase font-bold bg-secondaryDark text-white w-120px h-40px mt-5 rounded`} 44 | ${tw`sm:text-sm`} 45 | ${tw`hover:border hover:border-white cursor-pointer`} 46 | ` 47 | 48 | const MessageContainer = styled.div` 49 | ${tw`flex flex-row flex-grow w-8/12`} 50 | ` 51 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/DepositInfoGraphic.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | 4 | import React, { useEffect, useRef } from 'react' 5 | import * as THREE from 'assets/three.module' 6 | 7 | const DepositInfoGraphic = () => { 8 | const containerRef = useRef(null) 9 | useEffect(() => { 10 | let camera 11 | let scene 12 | let renderer 13 | let mesh 14 | let renderId 15 | const width = 65 16 | const height = 65 17 | 18 | const init = () => { 19 | camera = new THREE.PerspectiveCamera(55, width / height, 1, 1000) 20 | camera.position.z = 400 21 | 22 | scene = new THREE.Scene() 23 | 24 | const activeMaterial = new THREE.MeshBasicMaterial({ 25 | color: 0x000000, 26 | wireframe: true, 27 | opacity: 1.0, 28 | }) 29 | 30 | const geometry = new THREE.BoxBufferGeometry(200, 200, 200) 31 | mesh = new THREE.Mesh(geometry, activeMaterial) 32 | scene.add(mesh) 33 | 34 | renderer = new THREE.WebGLRenderer({ antialias: true }) 35 | renderer.setPixelRatio(window.devicePixelRatio) 36 | renderer.setSize(width, height) 37 | renderer.setClearColor(0xffffff, 1) 38 | 39 | camera.aspect = width / height 40 | camera.updateProjectionMatrix() 41 | 42 | if (containerRef.current) { 43 | containerRef.current.appendChild(renderer.domElement) 44 | } 45 | } 46 | 47 | const animate = () => { 48 | renderId = requestAnimationFrame(animate) 49 | mesh.rotation.x += 0.0025 50 | mesh.rotation.y += 0.005 51 | renderer.render(scene, camera) 52 | } 53 | 54 | const cleanup = () => { 55 | if (scene) { 56 | while (scene.children.length > 0) { 57 | scene.remove(scene.children[0]) 58 | } 59 | } 60 | if (renderer) { 61 | renderer.dispose() 62 | } 63 | cancelAnimationFrame(renderId) 64 | if (containerRef.current && containerRef.current.firstChild) { 65 | containerRef.current.removeChild(containerRef.current.firstChild) 66 | } 67 | } 68 | 69 | try { 70 | init() 71 | animate() 72 | } catch (e) { 73 | console.error('Unable to render graphic') 74 | } 75 | return cleanup 76 | }, []) 77 | 78 | return ( 79 | 80 | ) 81 | } 82 | 83 | const DepositInfoGraphicContainer = styled.div` 84 | ${tw`w-65px h-65px ml-4 mr-4 my-6`}; 85 | ` 86 | 87 | export default DepositInfoGraphic 88 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/GeyserFirstContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react' 2 | import { useParams, useNavigate } from 'react-router-dom' 3 | import styled from 'styled-components/macro' 4 | import tw from 'twin.macro' 5 | import { Overlay } from 'styling/styles' 6 | import { GeyserAction } from 'types' 7 | import Web3Context from 'context/Web3Context' 8 | import { GeyserContext } from 'context/GeyserContext' 9 | import { TabView } from 'components/TabView' 10 | import { ErrorPage } from 'components/ErrorPage' 11 | import PageLoader from 'components/PageLoader' 12 | import { GeyserStakeView } from './GeyserStakeView' 13 | import { GeyserStatsView } from './GeyserStatsView' 14 | 15 | export const GeyserFirstContainer = () => { 16 | const { slug } = useParams() 17 | const { ready, validNetwork } = useContext(Web3Context) 18 | const { 19 | geyserAction, 20 | updateGeyserAction, 21 | selectedGeyserInfo: { isWrapped }, 22 | selectGeyserBySlug, 23 | geysers, 24 | loading, 25 | } = useContext(GeyserContext) 26 | const actions = Object.values(GeyserAction) 27 | const navigate = useNavigate() 28 | 29 | const [geyserNotFound, setGeyserNotFound] = useState(false) 30 | useEffect(() => { 31 | const fetchGeyser = async () => { 32 | if (slug && geysers.length > 0) { 33 | const found = await selectGeyserBySlug(slug) 34 | setGeyserNotFound(!found) 35 | } 36 | } 37 | fetchGeyser() 38 | }, [slug, geysers, selectGeyserBySlug]) 39 | 40 | if (loading) return 41 | 42 | if (ready && validNetwork === false) { 43 | return navigate('/')} /> 44 | } 45 | 46 | if (geyserNotFound) { 47 | return navigate('/')} /> 48 | } 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 | 56 | 57 | updateGeyserAction(actions[a])} 60 | tabs={isWrapped ? ['Stake', 'Unstake', 'Wrapper'] : ['Stake', 'Unstake']} 61 | /> 62 | 63 | 64 | 65 | 66 | ) 67 | } 68 | 69 | const Container = styled.div` 70 | ${tw`text-center m-auto my-4 flex flex-col flex-wrap w-full`} 71 | ${tw`sm:w-sm`} 72 | ` 73 | 74 | const ToggleContainer = styled.div` 75 | ${tw`m-6`} 76 | ` 77 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/GeyserInteractionButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | 4 | interface Props { 5 | onClick: () => void 6 | displayText: string 7 | disabled?: boolean 8 | } 9 | 10 | export const GeyserInteractionButton: React.FC = ({ onClick, displayText, disabled }) => ( 11 | 14 | ) 15 | 16 | const Button = styled.button` 17 | ${tw`h-16 border-2 rounded-lg bg-secondary text-white uppercase font-semibold`}; 18 | ${tw`hover:border-white hover:bg-secondaryDark hover:text-white`} 19 | ${tw`disabled:bg-lightGray disabled:cursor-not-allowed disabled:border-none disabled:text-white`} 20 | ` 21 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/GeyserMultiStatsBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components/macro' 3 | import tw from 'twin.macro' 4 | import { animated } from 'react-spring' 5 | 6 | interface Stat { 7 | value: number 8 | units: string 9 | } 10 | 11 | interface Props { 12 | name: string 13 | stats: Stat[] 14 | from?: number 15 | interpolate?: (val: number) => string 16 | containerClassName?: string 17 | } 18 | 19 | export const GeyserMultiStatsBox: React.FC = ({ name, stats, interpolate, containerClassName }) => { 20 | const displayVal = (v: number): string => (interpolate ? interpolate(v) : `${v}`) 21 | const statsContent = stats.map((s, index) => ( 22 | 23 | 24 | {displayVal(s.value)} {s.units} 25 | 26 | {stats.length > 1 && index !== stats.length - 1 ? '\u00a0+\u00a0' : ''} 27 | 28 | )) 29 | 30 | return ( 31 | 32 | {name} 33 | {statsContent} 34 | 35 | ) 36 | } 37 | 38 | const GeyserStatsBoxContainer = styled.div` 39 | ${tw`sm:mr-5 sm:p-3 sm:h-72px`} 40 | ` 41 | 42 | const GeyserStatsBoxLabel = styled.span` 43 | ${tw`mb-1 flex font-light`} 44 | ` 45 | 46 | const GeyserStatsBoxValueContainer = styled.div` 47 | ${tw`flex flex-row`} 48 | ` 49 | 50 | const GeyserStatsBoxValue = styled.span`` 51 | 52 | const GeyserStatsBoxUnits = styled.span`` 53 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/GeyserStats.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | import { useContext } from 'react' 4 | import { StatsContext } from 'context/StatsContext' 5 | import { GeyserContext } from 'context/GeyserContext' 6 | import { safeNumeral } from 'utils/numeral' 7 | import TooltipTable from 'components/TooltipTable' 8 | import { GeyserStatsBox } from './GeyserStatsBox' 9 | import { DAY_IN_SEC, TOTAL_REWARDS_MSG } from '../../constants' 10 | 11 | export const GeyserStats = () => { 12 | const { 13 | geyserStats: { duration, totalDepositVal, totalRewards, totalRewardsVal }, 14 | } = useContext(StatsContext) 15 | const { 16 | selectedGeyserInfo: { 17 | rewardTokenInfo: { symbol: rewardTokenSymbol }, 18 | }, 19 | } = useContext(GeyserContext) 20 | 21 | return ( 22 | 23 |
Geyser Stats
24 | 25 | safeNumeral(val, '0.0')} 30 | /> 31 | 32 | 33 | safeNumeral(val, '0,0')} 38 | /> 39 | 40 | 41 | safeNumeral(val, '0,0')} 47 | tooltipMessage={{ 48 | title: 'Total Rewards', 49 | body: ( 50 |
51 | {TOTAL_REWARDS_MSG()} 52 | 63 |
64 | ), 65 | }} 66 | /> 67 |
68 |
69 | ) 70 | } 71 | 72 | const GeyserStatsContainer = styled.div` 73 | ${tw`px-5 my-5 pr-0`} 74 | ` 75 | 76 | const Header = styled.h3` 77 | ${tw`uppercase flex font-medium text-radicalRed`} 78 | ${tw`sm:pl-3`} 79 | ` 80 | 81 | const GeyserStatsBoxContainer = styled.div` 82 | ${tw`flex mt-4 sm:mt-3`} 83 | ` 84 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/GeyserStatsView.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | import { MyStats } from './MyStats' 4 | import { GeyserStats } from './GeyserStats' 5 | 6 | export const GeyserStatsView = () => ( 7 | 8 | 9 | 10 | 11 | ) 12 | 13 | const GeyserStatsContainer = styled.div` 14 | ${tw`grid grid-cols-2 w-full h-280px`}; 15 | ${tw`sm:h-312px`} 16 | ` 17 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/MyStatsBox.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | import { useSpring, animated } from 'react-spring' 4 | import { useState } from 'react' 5 | 6 | interface Props { 7 | name: string 8 | from?: number 9 | interpolate: (val: number) => string 10 | value: number 11 | units: string 12 | delim?: string 13 | classNames?: string 14 | } 15 | 16 | export const MyStatsBox: React.FC = ({ 17 | classNames, 18 | name, 19 | units, 20 | delim, 21 | value: targetValue, 22 | from, 23 | interpolate, 24 | }) => { 25 | const [statsValue, setStatsValue] = useState(interpolate(targetValue)) 26 | 27 | useSpring({ 28 | val: targetValue, 29 | from: { val: from || 0 }, 30 | onChange: ({ value }) => { 31 | setStatsValue(interpolate(value.val)) 32 | }, 33 | }) 34 | 35 | return ( 36 | 37 | {name} 38 | 39 | 40 | {statsValue} 41 | {delim} 42 | {units} 43 | 44 | 45 | 46 | ) 47 | } 48 | 49 | const MyStatContainer = styled.div` 50 | ${tw`h-40px mt-4`} 51 | ${tw`sm:my-5 sm:col-span-1 sm:h-fit sm:h-72px`} 52 | ` 53 | 54 | const MyStatName = styled.span` 55 | ${tw`mb-1 flex font-light`} 56 | ${tw`sm:mb-2 sm:mr-8 sm:block sm:ml-3`} 57 | ` 58 | 59 | const MyStatValueContainer = styled.div` 60 | ${tw`flex`} 61 | ${tw`sm:rounded-md sm:bg-mediumGray sm:text-white sm:mt-2 sm:py-7 sm:items-center sm:justify-center sm:h-80px sm:w-80px px-2`} 62 | ` 63 | 64 | const MyStatValue = styled.span` 65 | ${tw`w-full text-left sm:text-center sm:font-bold`} 66 | ` 67 | 68 | const MyStatUnits = styled.span`` 69 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/StakeWarning.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BigNumber } from 'ethers' 3 | import tw from 'twin.macro' 4 | import styled from 'styled-components/macro' 5 | 6 | interface Props { 7 | poolAddress: string 8 | balance: BigNumber 9 | staked: BigNumber 10 | otherActiveLock: boolean 11 | } 12 | 13 | export const StakeWarning: React.FC = ({ poolAddress, balance, staked, otherActiveLock }) => { 14 | const renderStakeWarning = (message: string, buttonLabel: string, url: string, newTab: bool) => ( 15 | 16 | 17 | 18 | 19 | {message} 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | 28 | if (otherActiveLock) { 29 | return renderStakeWarning('Your tokens are staked elsewhere', 'Unstake', '/', false) 30 | } 31 | 32 | if (balance.gte(0)) { 33 | return null 34 | } 35 | 36 | const buttonLabel = staked.lte(0) ? 'Get LP' : 'Get more' 37 | return renderStakeWarning('Insufficient balance', buttonLabel, poolAddress, true) 38 | } 39 | 40 | const StakeWarningContainer = styled.div` 41 | ${tw`h-80px mt-1 mb-5 border border-lightGray flex flex-row tracking-wider`} 42 | ` 43 | 44 | const ColoredDiv = styled.div` 45 | ${tw`h-full w-2 bg-secondaryDark`} 46 | ` 47 | 48 | const Content = styled.div` 49 | ${tw`flex flex-row flex-grow text-white bg-secondary font-bold`} 50 | ` 51 | 52 | const MessageContainer = styled.div` 53 | ${tw`flex flex-row flex-grow w-8/12`} 54 | ` 55 | 56 | const Message = styled.span` 57 | ${tw`ml-5 my-auto`} 58 | ` 59 | 60 | const ButtonWrapper = styled.div` 61 | ${tw`flex-grow w-2/12`} 62 | ` 63 | 64 | const Button = styled.button` 65 | ${tw`uppercase font-bold bg-secondaryDark text-white w-btnsm h-btnsm mt-5 rounded flex-grow`} 66 | ${tw`sm:text-sm`} 67 | ${tw`hover:border hover:border-white cursor-pointer`} 68 | ` 69 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/UnstakeConfirmModal.tsx: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | import styled from 'styled-components/macro' 3 | import tw from 'twin.macro' 4 | import { ModalButton } from 'styling/styles' 5 | import { Modal } from 'components/Modal' 6 | 7 | interface Props { 8 | open: boolean 9 | onClose: () => void 10 | onConfirm: () => void 11 | parsedUserInput: BigNumber 12 | } 13 | 14 | export const UnstakeConfirmModal: React.FC = ({ open, onClose, onConfirm }) => ( 15 | 16 | Are you sure? 17 | If you stayed deposited, you could be eligible for an additional rewards. 18 | 19 | 20 | No, Wait 21 | 22 | Withdraw Anyway 23 | 24 | 25 | ) 26 | 27 | const ConfirmButton = styled(ModalButton)` 28 | ${tw`rounded-lg bg-primary text-white font-semibold`} 29 | ` 30 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/UnstakeSummary.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState, useEffect } from 'react' 2 | import styled from 'styled-components/macro' 3 | import tw from 'twin.macro' 4 | import { BigNumber } from 'ethers' 5 | import { GeyserContext } from 'context/GeyserContext' 6 | import { StatsContext } from 'context/StatsContext' 7 | import { CardValue, CardLabel } from 'styling/styles' 8 | import { formatTokenBalance } from 'utils/amount' 9 | import { safeNumeral } from 'utils/numeral' 10 | 11 | interface Props { 12 | userInput: string 13 | parsedUserInput: BigNumber 14 | } 15 | 16 | export const UnstakeSummary: React.FC = ({ userInput, parsedUserInput }) => { 17 | const { 18 | selectedGeyserInfo: { 19 | rewardTokenInfo: { symbol: rewardTokenSymbol, price: rewardTokenPrice }, 20 | stakingTokenInfo: { symbol: stakingTokenSymbol, price: stakingTokenPrice }, 21 | }, 22 | } = useContext(GeyserContext) 23 | const { computeRewardsFromUnstake, computeRewardsShareFromUnstake } = useContext(StatsContext) 24 | 25 | const [rewardAmount, setRewardAmount] = useState(0.0) 26 | // const [rewardsShare, setRewardsShare] = useState(0.0) 27 | 28 | const unstakeUSD = parseFloat(userInput) * stakingTokenPrice 29 | const rewardUSD = rewardAmount * rewardTokenPrice 30 | // TODO: handle bonus rewards 31 | // bonusRewards.reduce((m, b) => m + rewardsShare * b.value, 0) 32 | useEffect(() => { 33 | let isMounted = true 34 | ;(async () => { 35 | try { 36 | const computedRewardAmount = await computeRewardsFromUnstake(parsedUserInput) 37 | const computedRewardsShare = await computeRewardsShareFromUnstake(parsedUserInput) 38 | if (isMounted) { 39 | setRewardAmount(computedRewardAmount) 40 | setRewardsShare(computedRewardsShare) 41 | } 42 | } catch (e) { 43 | if (isMounted) { 44 | console.log('Error: user input higher than user stake') 45 | } 46 | } 47 | })() 48 | return () => { 49 | isMounted = false 50 | } 51 | }, [parsedUserInput, computeRewardsFromUnstake, computeRewardsShareFromUnstake]) 52 | 53 | return ( 54 | 55 | 56 | 57 | 61 | 62 | {formatTokenBalance(userInput)} 63 | {stakingTokenSymbol} 64 | 65 | 66 | 67 | 68 | 69 | 73 | 74 | {safeNumeral(rewardAmount, '0.000')} 75 | {rewardTokenSymbol} 76 | 77 | 78 | {/* {bonusRewards.map((b) => ( 79 | 80 | {safeNumeral(rewardsShare * b.balance, '0.000')} 81 | {b.symbol} 82 | 83 | ))} */} 84 | 85 | 86 | 87 | ) 88 | } 89 | 90 | const Container = styled.div` 91 | ${tw`grid grid-cols-2 gap-x-4 my-6`} 92 | ` 93 | 94 | const SummaryCard = styled.div` 95 | ${tw`h-120px border border-lightGray rounded flex flex-col my-auto tracking-wide`} 96 | ` 97 | 98 | const Label = styled(CardLabel)` 99 | ${tw`text-sm sm:text-base text-left`} 100 | ` 101 | 102 | const Value = styled(CardValue)` 103 | ${tw`text-sm sm:text-base flex-wrap text-left`} 104 | ` 105 | 106 | const Amount = styled.span` 107 | ${tw`whitespace-pre-wrap`} 108 | ` 109 | 110 | const Content = styled.div` 111 | ${tw`flex flex-col my-auto ml-4`} 112 | ` 113 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/UserBalance.tsx: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from 'ethers' 2 | import styled from 'styled-components/macro' 3 | import tw from 'twin.macro' 4 | import { formatUnits } from 'ethers/lib/utils' 5 | import { formatTokenBalance } from 'utils/amount' 6 | 7 | interface Props { 8 | parsedAmount: BigNumber 9 | currentAmount: BigNumber 10 | decimals: number 11 | symbol: string 12 | isStakingAction: boolean 13 | poolAddress?: string 14 | } 15 | 16 | export const UserBalance: React.FC = ({ 17 | parsedAmount, 18 | currentAmount, 19 | decimals, 20 | symbol, 21 | isStakingAction, 22 | poolAddress, 23 | }) => { 24 | const formatDisplayAmount = (amt: BigNumberish, sym: string) => ( 25 | 26 | {formatTokenBalance(formatUnits(amt, decimals))} {sym} 27 | 28 | ) 29 | 30 | if (isStakingAction) { 31 | const avail = currentAmount.sub(parsedAmount) 32 | return ( 33 | 34 | Available balance: {formatDisplayAmount(avail.lte(0) ? 0 : avail, symbol)} 35 | 36 | ) 37 | } else { 38 | return ( 39 | 40 | {parsedAmount.isZero() ? ( 41 | Staked balance: {formatDisplayAmount(currentAmount, symbol)} 42 | ) : ( 43 | Remaining staked balance: {formatDisplayAmount(currentAmount.sub(parsedAmount), symbol)} 44 | )} 45 | 46 | ) 47 | } 48 | } 49 | 50 | const Text = styled.span` 51 | ${tw`text-xs sm:text-sm`} 52 | ` 53 | 54 | const BalLink = styled.a` 55 | ${tw`hover:underline`} 56 | ` 57 | 58 | const FlexDiv = styled.div` 59 | ${tw`flex`} 60 | ` 61 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/WithdrawTxMessage.tsx: -------------------------------------------------------------------------------- 1 | import { TxStateMachine } from 'hooks/useTxStateMachine' 2 | import { EtherscanLink } from 'components/EtherscanLink' 3 | import { formatTokenBalance } from 'utils/amount' 4 | import { TxState } from '../../constants' 5 | 6 | interface Props { 7 | txStateMachine: TxStateMachine 8 | symbol: string 9 | amount: string 10 | successMessage?: string 11 | errorMessage?: string 12 | } 13 | 14 | export const WithdrawTxMessage: React.FC = ({ 15 | txStateMachine: { state, response }, 16 | symbol, 17 | amount, 18 | successMessage, 19 | errorMessage, 20 | }) => { 21 | const getTxMessage = () => { 22 | switch (state) { 23 | case TxState.PENDING: 24 | return ( 25 | 26 | Withdrawing {symbol} to your wallet... 27 | 28 | ) 29 | case TxState.SUBMITTED: 30 | return ( 31 | 32 | Submitted {symbol} withdrawal transaction. View on 33 | 34 | ) 35 | case TxState.MINED: 36 | return ( 37 | 38 | Successfully withdrew{' '} 39 | 40 | {formatTokenBalance(amount)} {symbol} 41 | {' '} 42 | to your wallet. View on . {successMessage} 43 | 44 | ) 45 | case TxState.FAILED: 46 | return ( 47 | 48 | Unlocked{' '} 49 | 50 | {formatTokenBalance(amount)} {symbol} 51 | {' '} 52 | from the vault. {errorMessage} 53 | 54 | ) 55 | default: 56 | return <> 57 | } 58 | } 59 | return getTxMessage() 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/WrapperCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | 4 | interface Props { 5 | checked: boolean 6 | onChange: (v: boolean) => void 7 | } 8 | 9 | export const WrapperCheckbox: React.FC = ({ checked, onChange }) => ( 10 | 11 | onChange(!checked)}> 12 | {}} /> 13 |   Wrap and deposit into vault for staking 14 | 15 | 16 | ) 17 | 18 | const Text = styled.span` 19 | ${tw`text-xs sm:text-sm cursor-pointer`} 20 | ` 21 | 22 | const Input = styled.input` 23 | ${tw`cursor-pointer`} 24 | ` 25 | 26 | const FlexDiv = styled.div` 27 | ${tw`flex mb-2`} 28 | ` 29 | -------------------------------------------------------------------------------- /frontend/src/components/GeyserFirst/WrapperWarning.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | 4 | interface Props {} 5 | 6 | export const WrapperWarning: React.FC = () => ( 7 | 8 | 9 | NOTE: Liquidity token needs to be wrapped before staking 10 | 11 | 12 | ) 13 | 14 | const Text = styled.span` 15 | ${tw`text-xs sm:text-sm`} 16 | ` 17 | 18 | const FlexDiv = styled.div` 19 | ${tw`flex`} 20 | ` 21 | -------------------------------------------------------------------------------- /frontend/src/components/GeysersList.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | import { useContext } from 'react' 4 | import { GeyserContext } from 'context/GeyserContext' 5 | import { VaultContext } from 'context/VaultContext' 6 | import { useNavigate } from 'react-router-dom' 7 | import { Dropdown } from './Dropdown' 8 | 9 | export const GeysersList = () => { 10 | const navigate = useNavigate() 11 | const { 12 | geysers, 13 | getGeyserSlugByName, 14 | selectedGeyserInfo: { geyser: selectedGeyser }, 15 | getGeyserName, 16 | } = useContext(GeyserContext) 17 | const { selectedVault } = useContext(VaultContext) 18 | 19 | const handleGeyserChange = async (geyserName: string) => { 20 | navigate(`/geysers/${getGeyserSlugByName(geyserName)}`) 21 | } 22 | 23 | const optgroups = (() => { 24 | const stakedGeysers = selectedVault ? selectedVault.locks.map((l) => l.geyser).filter((g) => !!g) : [] 25 | let geysersToShow = geysers.filter((g) => g.active || stakedGeysers.find((s) => s.id === g.id)) 26 | if (geysersToShow.length === 0) { 27 | geysersToShow = geysers.slice(0, 3) 28 | } 29 | 30 | const activeGeysers = geysersToShow.filter((g) => g.active === true).map(({ id }) => getGeyserName(id)) 31 | const inactiveGeysers = geysersToShow.filter((g) => !(g.active === true)).map(({ id }) => getGeyserName(id)) 32 | const options = [] 33 | if (activeGeysers.length > 0) { 34 | options.push({ 35 | group: 'Active Geysers', 36 | options: activeGeysers, 37 | }) 38 | } 39 | if (inactiveGeysers.length > 0) { 40 | options.push({ 41 | group: 'Inactive Geysers', 42 | options: inactiveGeysers, 43 | }) 44 | } 45 | return options 46 | })() 47 | 48 | return ( 49 | <> 50 | {geysers.length > 0 && ( 51 | 52 | 53 | 54 | 55 | 60 | 61 | )} 62 | 63 | ) 64 | } 65 | 66 | const GeysersListContainer = styled.div` 67 | ${tw`my-3`} 68 | ${tw`mx-5 sm:mx-10 xl:mx-5`} 69 | ` 70 | 71 | const Heading = styled.div` 72 | ${tw`flex flex-row`} 73 | ` 74 | 75 | const Label = styled.span` 76 | ${tw`tracking-wider`} 77 | ` 78 | -------------------------------------------------------------------------------- /frontend/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Web3Context from 'context/Web3Context' 2 | import { GeyserContext } from 'context/GeyserContext' 3 | import { VaultContext } from 'context/VaultContext' 4 | import { useContext } from 'react' 5 | import styled from 'styled-components/macro' 6 | import tw from 'twin.macro' 7 | import { Tab } from '@headlessui/react' 8 | import { useLocation, useNavigate } from 'react-router-dom' 9 | import { HeaderWalletButton } from './HeaderWalletButton' 10 | 11 | export const HeaderTab: React.FC = () => { 12 | const { selectedGeyserConfig, getDefaultGeyserConfig } = useContext(GeyserContext) 13 | const location = useLocation() 14 | const navigate = useNavigate() 15 | 16 | const defaultGeyser = getDefaultGeyserConfig() 17 | let selectedIndex = 0 18 | if (/^\/geysers\/[^/]+$/.test(location.pathname)) { 19 | selectedIndex = 1 20 | } else if (location.pathname === '/vault') { 21 | selectedIndex = 2 22 | } 23 | return ( 24 | { 27 | if (index === 0) { 28 | navigate('/') 29 | } else if (index === 1) { 30 | navigate(`/geysers/${selectedGeyserConfig?.slug || defaultGeyser?.slug}`) 31 | } else if (index === 2) { 32 | navigate('/vault') 33 | } 34 | }} 35 | > 36 | 37 | 38 | Home 39 | 40 | Stake 41 | Vault 42 | 43 | 44 | ) 45 | } 46 | 47 | export const Header = () => { 48 | const { ready } = useContext(Web3Context) 49 | const { vaults, loading } = useContext(VaultContext) 50 | const navigate = useNavigate() 51 | const showTabs = ready && (loading || vaults.length > 0) 52 | if (showTabs) { 53 | return ( 54 | 55 | 56 | navigate('/')}>Λ 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ) 66 | } 67 | 68 | return ( 69 | 70 | 71 | 72 | navigate('/')}>Λ 73 | 74 | 75 | 76 | 77 | 78 | ) 79 | } 80 | 81 | const Container = styled.div` 82 | ${tw`flex flex-wrap py-1 h-fit items-center justify-center`} 83 | ${tw`bg-white border-b border-lightGray shadow py-2`} 84 | ` 85 | 86 | const LogoSpan = styled.a` 87 | font-family: 'Coromont Garamond'; 88 | ${tw`p-3 text-3xl w-full`} 89 | ${tw`cursor-pointer`} 90 | ` 91 | 92 | const LeftContainer = styled.div` 93 | ${tw`flex w-auto`} 94 | ${tw`w-4/12 text-right`} 95 | ` 96 | 97 | const MiddleContainer = styled.div` 98 | ${tw`flex flex-col xl:flex-row items-center justify-center w-full order-3 py-6`} 99 | ${tw`py-0 max-w-md mx-auto order-2 w-1/3 xl:w-4/12 text-center`} 100 | ` 101 | 102 | const RightContainer = styled.div` 103 | ${tw`ml-auto order-2 w-auto flex flex-wrap`} 104 | ${tw`ml-0 order-3 w-4/12`} 105 | ` 106 | 107 | const HeaderTabItem = styled(Tab).withConfig({ 108 | shouldForwardProp: (prop) => prop !== 'isSelected', 109 | })<{ isSelected: boolean }>` 110 | ${tw`font-normal tracking-wider px-4 py-2 text-center cursor-pointer`} 111 | ${({ isSelected }) => (isSelected ? tw`text-black font-bold` : tw`text-gray hover:text-black`)}; 112 | ${({ isSelected }) => isSelected && `background-color: #f9f9f9;`} 113 | ${tw`focus:outline-none focus:ring-0`} 114 | ` 115 | -------------------------------------------------------------------------------- /frontend/src/components/HeaderNetworkSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import styled from 'styled-components/macro' 3 | import tw from 'twin.macro' 4 | import { activeNetworks, getConnectionConfig } from 'config/app' 5 | import { Option, Select } from 'components/Select' 6 | import Web3Context from 'context/Web3Context' 7 | 8 | interface Props {} 9 | 10 | export const HeaderNetworkSelect: React.FC = () => { 11 | const { selectNetwork, networkId } = useContext(Web3Context) 12 | 13 | const networkConfigs = activeNetworks.map((n) => getConnectionConfig(n)) 14 | const networkOptions: Option[] = networkConfigs.map((n) => ({ 15 | id: `${n.id}`, 16 | name: n.name, 17 | })) 18 | const selectedOption = networkOptions.findIndex((o) => o.id === `${networkId}`) 19 | const selected = selectedOption === -1 ? 0 : selectedOption 20 | 21 | return ( 22 | 23 | 49 | 50 | 51 | ) 52 | } 53 | 54 | const Container = styled.div` 55 | ${tw`flex flex-row border border-gray h-fit mb-3 mt-1 rounded-md`} 56 | ` 57 | 58 | const Input = styled.input` 59 | ::-webkit-inner-spin-button { 60 | -webkit-appearance: none; 61 | margin: 0; 62 | } 63 | ::-webkit-outer-spin-button { 64 | -webkit-appearance: none; 65 | margin: 0; 66 | } 67 | ${tw`w-10/12 font-semibold tracking-wider rounded-lg p-3 text-base`} 68 | ${tw`focus:outline-none`} 69 | ` 70 | 71 | const Button = styled.button` 72 | ${tw`uppercase focus:outline-none p-1 text-sm w-2/12 text-link bg-0D23EE bg-opacity-5`} 73 | ` 74 | -------------------------------------------------------------------------------- /frontend/src/components/ProcessingButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | import { ModalButton } from 'styling/styles' 4 | 5 | export const ProcessingButton = () => Processing 6 | 7 | const DisabledButton = styled(ModalButton)` 8 | ${tw`bg-lightGray cursor-not-allowed border-none text-white cursor-not-allowed`} 9 | ` 10 | -------------------------------------------------------------------------------- /frontend/src/components/Select.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react' 2 | import { Listbox, Transition } from '@headlessui/react' 3 | import { CheckIcon, SelectorIcon } from '@heroicons/react/solid' 4 | import styled from 'styled-components/macro' 5 | import tw from 'twin.macro' 6 | 7 | export interface Option { 8 | id: string 9 | name: string 10 | } 11 | 12 | interface Props { 13 | selected: number 14 | options: Option[] 15 | onChange?: (option: number) => void 16 | disabled?: boolean 17 | } 18 | 19 | const StyledListboxButton = styled(Listbox.Button)` 20 | ${tw`relative w-full mt-2 mb-2 py-2 pl-3 pr-10 text-left bg-white rounded-md shadow-md sm:text-lg shadow-none flex flex-row border border-gray h-fit mb-3 mt-1 rounded-md`} 21 | ${tw`cursor-default focus:outline-none focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-white focus-visible:ring-offset-primary focus-visible:ring-offset-2 focus-visible:border-primary`} 22 | ` 23 | 24 | const StyledListboxOptions = styled(Listbox.Options)` 25 | ${tw`absolute w-full py-1 mt-1 overflow-auto text-base text-left bg-white rounded-md shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 sm:text-lg`} 26 | ${tw`focus:outline-none z-10`} 27 | ` 28 | 29 | const StyledSelectorIconContainer = styled.span` 30 | ${tw`absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none`} 31 | ` 32 | 33 | const StyledSelectorIcon = styled(SelectorIcon)` 34 | ${tw`w-5 h-5 text-darkGray`} 35 | ` 36 | 37 | const StyledCheckIconContainer = styled.span` 38 | ${tw`absolute inset-y-0 left-0 flex items-center pl-3`} 39 | ` 40 | 41 | const StyledCheckIcon = styled(CheckIcon)` 42 | ${tw`w-5 h-5`} 43 | ` 44 | 45 | const noOp = () => {} 46 | 47 | export const Select: React.FC = ({ selected, options, onChange, disabled }) => ( 48 | 49 |
50 | 51 | {options[selected].name} 52 | {!disabled ? ( 53 | 54 | 55 | 56 | ) : null} 57 | 58 | 59 | 60 | {options.map((o, i) => ( 61 | `cursor-default relative py-2 pl-10 pr-4 ${active ? 'bg-lightGray' : null}`} 64 | value={i} 65 | > 66 | {({ selected: optionSelected }) => ( 67 | <> 68 | {o.name} 69 | {optionSelected ? ( 70 | 71 | 72 | 73 | ) : null} 74 | 75 | )} 76 | 77 | ))} 78 | 79 | 80 |
81 |
82 | ) 83 | -------------------------------------------------------------------------------- /frontend/src/components/SingleTxMessage.tsx: -------------------------------------------------------------------------------- 1 | import { TxStateMachine } from 'hooks/useTxStateMachine' 2 | import { ReactNode } from 'react' 3 | import { TxState } from '../constants' 4 | import { EtherscanLink } from './EtherscanLink' 5 | 6 | interface Props { 7 | txStateMachine: TxStateMachine 8 | successMessage: ReactNode 9 | } 10 | 11 | export const SingleTxMessage: React.FC = ({ txStateMachine: { state, response }, successMessage }) => { 12 | const getTxMessage = () => { 13 | switch (state) { 14 | case TxState.PENDING: 15 | return Waiting for transaction confirmation... 16 | case TxState.SUBMITTED: 17 | return ( 18 | 19 | Transaction submitted to blockchain, waiting to be mined. View on 20 | 21 | ) 22 | case TxState.MINED: 23 | return ( 24 | <> 25 | {successMessage}{' '} 26 | 27 | View on . 28 | 29 | 30 | ) 31 | case TxState.FAILED: 32 | return <>Transaction failed. 33 | default: 34 | return <> 35 | } 36 | } 37 | return getTxMessage() 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/components/SingleTxModal.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useState } from 'react' 2 | import { TransactionResponse } from '@ethersproject/providers' 3 | import { useTxStateMachine } from 'hooks/useTxStateMachine' 4 | import { ModalButton } from 'styling/styles' 5 | import { Modal } from './Modal' 6 | import { ProcessingButton } from './ProcessingButton' 7 | import { TxState } from '../constants' 8 | import { SingleTxMessage } from './SingleTxMessage' 9 | 10 | interface Props { 11 | submit: () => Promise 12 | txSuccessMessage?: ReactNode 13 | open: boolean 14 | onClose: () => void 15 | } 16 | 17 | export const SingleTxModal: React.FC = ({ submit, txSuccessMessage, open, onClose, children }) => { 18 | const txStateMachine = useTxStateMachine(submit) 19 | const { state, submitTx, refresh } = txStateMachine 20 | const [successMessage, setSuccessMessage] = useState(null) 21 | 22 | useEffect(() => { 23 | if (open) { 24 | refresh() 25 | setSuccessMessage(txSuccessMessage || null) 26 | submitTx() 27 | } 28 | }, [open]) 29 | 30 | const isProcessing = () => [TxState.PENDING, TxState.SUBMITTED].includes(state) 31 | 32 | return ( 33 | 34 | Processing Transaction 35 | 36 | 37 | {children} 38 | 39 | 40 | {isProcessing() ? : Close } 41 | 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | // put in a component instead of its own asset file, because we need tailwind classes 2 | export const Spinner = () => ( 3 | 4 | 5 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /frontend/src/components/TabView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tab } from '@headlessui/react' 3 | import styled from 'styled-components/macro' 4 | import tw from 'twin.macro' 5 | 6 | interface Props { 7 | active: number 8 | tabs: string[] 9 | onChange: (tab: number) => void 10 | } 11 | 12 | export const TabView: React.FC = ({ active, tabs, onChange }) => { 13 | const StyledTabList = styled(Tab.List)` 14 | ${tw`bg-darkGray relative rounded m-auto flex border border-darkGray`} 15 | ` 16 | 17 | const StyledTab = styled(Tab)` 18 | ${tw`outline-none focus:outline-none`} 19 | ${tw`font-bold uppercase h-full block rounded self-center`} 20 | ` 21 | 22 | const FlexTabWidth2 = styled(StyledTab)` 23 | ${tw`w-1/2`} 24 | ` 25 | 26 | const FlexTabWidth3 = styled(StyledTab)` 27 | ${tw`w-1/3`} 28 | ` 29 | 30 | const StyledFlexTab = tabs.length === 2 ? FlexTabWidth2 : FlexTabWidth3 31 | 32 | return ( 33 | 34 | 35 | {tabs.map((t, i) => ( 36 | 37 | {t} 38 | 39 | ))} 40 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/components/ThreeTabView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Switch } from '@headlessui/react' 3 | import styled from 'styled-components/macro' 4 | import tw from 'twin.macro' 5 | 6 | interface Props { 7 | enabled: boolean 8 | options: [string, string, string] 9 | toggle: () => void 10 | } 11 | 12 | export const ThreeTabView: React.FC = ({ enabled, toggle, options }) => ( 13 | 14 | 15 | {options[0]} 16 | 21 | {options[1]} 22 | {options[2]} 23 | 24 | 25 | ) 26 | 27 | const SwitchContainer = styled.span` 28 | ${tw`bg-darkGray relative rounded m-auto flex border border-darkGray`} 29 | ` 30 | 31 | const SwitchOptionOne = styled.span` 32 | ${tw`font-bold uppercase absolute z-10 w-1/3 self-center`} 33 | ` 34 | 35 | const SwitchOptionTwo = styled.span` 36 | ${tw`font-bold uppercase z-10 w-1/3 self-center`} 37 | ` 38 | 39 | const SwitchOptionThree = styled.span` 40 | ${tw`font-bold uppercase z-10 w-1/3 self-center`} 41 | ` 42 | -------------------------------------------------------------------------------- /frontend/src/components/ToggleView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Switch } from '@headlessui/react' 3 | import styled from 'styled-components/macro' 4 | import tw from 'twin.macro' 5 | 6 | interface Props { 7 | enabled: boolean 8 | options: [string, string] 9 | toggle: () => void 10 | } 11 | 12 | export const ToggleView: React.FC = ({ enabled, toggle, options }) => ( 13 | 14 | 15 | {options[0]} 16 | 21 | {options[1]} 22 | 23 | 24 | ) 25 | 26 | const SwitchContainer = styled.span` 27 | ${tw`bg-darkGray relative rounded m-auto flex border border-darkGray`} 28 | ` 29 | 30 | const SwitchOptionOne = styled.span` 31 | ${tw`font-bold uppercase absolute z-10 w-1/2 self-center`} 32 | ` 33 | 34 | const SwitchOptionTwo = styled.span` 35 | ${tw`font-bold uppercase z-10 w-1/2 self-center`} 36 | ` 37 | -------------------------------------------------------------------------------- /frontend/src/components/TokenIcons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import amplIcon from 'assets/tokens/ampl.png' 4 | import forthIcon from 'assets/tokens/forth.png' 5 | import spotIcon from 'assets/tokens/spot.png' 6 | import usdcIcon from 'assets/tokens/usdc.png' 7 | import wamplIcon from 'assets/tokens/wampl.png' 8 | import wbtcIcon from 'assets/tokens/wbtc.png' 9 | import wethIcon from 'assets/tokens/weth.png' 10 | 11 | function getTokenIcon(token) { 12 | switch (token.toLowerCase()) { 13 | case 'ampl': 14 | return amplIcon 15 | case 'forth': 16 | return forthIcon 17 | case 'spot': 18 | return spotIcon 19 | case 'usdc': 20 | return usdcIcon 21 | case 'wampl': 22 | return wamplIcon 23 | case 'wbtc': 24 | return wbtcIcon 25 | case 'weth': 26 | return wethIcon 27 | default: 28 | return amplIcon 29 | } 30 | } 31 | 32 | const TokenIcons = ({ tokens }) => ( 33 |
34 | {tokens.map((token, index) => ( 35 |
0 ? '-ml-3' : ''} 41 | `} 42 | > 43 | {token} 44 |
45 | ))} 46 |
47 | ) 48 | 49 | export default TokenIcons 50 | -------------------------------------------------------------------------------- /frontend/src/components/ToolButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | 4 | interface Props { 5 | displayText: string 6 | classNames?: string 7 | onClick: () => void 8 | } 9 | 10 | export const ToolButton: React.FC = ({ classNames, displayText, onClick, children }) => ( 11 | 14 | ) 15 | 16 | const Button = styled.button` 17 | ${tw`p-0 inline-flex uppercase text-link`} 18 | ${tw`hover:underline`} 19 | ` 20 | -------------------------------------------------------------------------------- /frontend/src/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Popover, Transition } from '@headlessui/react' 2 | import { Fragment, useState } from 'react' 3 | import styled from 'styled-components/macro' 4 | import tw from 'twin.macro' 5 | import { InformationCircleIcon } from '@heroicons/react/outline' 6 | import { TooltipMessage } from 'types' 7 | 8 | interface Props { 9 | messages: TooltipMessage[] 10 | classNames?: string 11 | panelClassnames?: string 12 | } 13 | 14 | export const Tooltip: React.FC = ({ messages, classNames, panelClassnames }) => { 15 | const [isOpen, setIsOpen] = useState(false) 16 | 17 | const handleMouseEnter = () => { 18 | if (!isOpen) setIsOpen(true) 19 | } 20 | 21 | const handleClick = () => setIsOpen((prev) => !prev) 22 | const handleOutsideClick = () => setIsOpen(false) 23 | 24 | return ( 25 | 26 | 31 | 32 | 33 | 43 | setIsOpen(true)} 46 | onMouseLeave={handleOutsideClick} 47 | > 48 | 49 | 50 | {messages.map(({ title, body }) => ( 51 | 52 | {title} 53 | {body} 54 | 55 | ))} 56 | 57 | 58 | 59 | 60 | 61 | ) 62 | } 63 | 64 | const OuterLayer = styled.div` 65 | ${tw`shadow-all max-w-sm rounded-lg overflow-hidden ring-1 ring-black ring-opacity-5`} 66 | ` 67 | 68 | const InnerLayer = styled.div` 69 | ${tw`relative gap-6 bg-black p-6`} 70 | ` 71 | 72 | const Message = styled.div` 73 | ${tw`m-auto`} 74 | ` 75 | 76 | const Title = styled.p` 77 | ${tw`text-gray mb-2 text-lg`} 78 | ` 79 | 80 | const Body = styled.p` 81 | ${tw`text-white text-left font-semiBold sm:leading-5 text-sm`} 82 | ` 83 | -------------------------------------------------------------------------------- /frontend/src/components/TooltipTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components/macro' 3 | import tw from 'twin.macro' 4 | 5 | interface TableRowProps { 6 | label: string 7 | value: string 8 | } 9 | 10 | interface TooltipTableProps { 11 | rows: TableRowProps[] 12 | totalLabel: string 13 | totalValue: string 14 | } 15 | 16 | const TooltipTable: React.FC = ({ rows, totalLabel, totalValue }) => ( 17 | 18 | 19 | 20 | {rows.map((row) => ( 21 | 22 | {row.label} 23 | {row.value} 24 | 25 | ))} 26 | 27 | 28 | {totalLabel} 29 | {totalValue} 30 | 31 | 32 |
33 |
34 | ) 35 | 36 | export default TooltipTable 37 | 38 | const TooltipTableContainer = styled.div` 39 | ${tw`p-4 bg-gray w-full shadow-md rounded mt-5`} 40 | ` 41 | 42 | const Table = styled.table` 43 | ${tw`w-full`} 44 | ` 45 | 46 | const TableRow = styled.tr` 47 | ${tw`flex justify-between items-center text-white mb-1`} 48 | ` 49 | 50 | const TableCellLabel = styled.td` 51 | ${tw`text-sm text-white`} 52 | ` 53 | 54 | const TableCellValue = styled.td` 55 | ${tw`text-sm text-white font-semibold`} 56 | ` 57 | 58 | const TableRowTotal = styled.tr` 59 | ${tw`flex justify-between items-center text-white border-t mt-1 pt-1`} 60 | ` 61 | 62 | const TableCellLabelTotal = styled.td` 63 | ${tw`text-base text-white font-bold`} 64 | ` 65 | 66 | const TableCellValueTotal = styled.td` 67 | ${tw`text-base text-white font-bold`} 68 | ` 69 | 70 | const TableDivider = styled.tr` 71 | ${tw`border-t border-gray`} 72 | ` 73 | -------------------------------------------------------------------------------- /frontend/src/components/VaultFirst/VaultFirstContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import styled from 'styled-components/macro' 3 | import { CardLabel, Overlay } from 'styling/styles' 4 | import tw from 'twin.macro' 5 | import { VaultContext } from 'context/VaultContext' 6 | import Web3Context from 'context/Web3Context' 7 | import { useNavigate } from 'react-router-dom' 8 | import PageLoader from 'components/PageLoader' 9 | import { ErrorPage } from 'components/ErrorPage' 10 | import { Tooltip } from 'components/Tooltip' 11 | import { VaultBalanceView } from './VaultBalanceView' 12 | import { UNIVERSAL_VAULT_MSG } from '../../constants' 13 | 14 | export const VaultFirstContainer = () => { 15 | const { ready, connectWallet, validNetwork } = useContext(Web3Context) 16 | const { vaults, loading } = useContext(VaultContext) 17 | const navigate = useNavigate() 18 | 19 | if (!ready) { 20 | return connectWallet()} /> 21 | } 22 | 23 | if (ready && validNetwork === false) { 24 | return navigate('/')} /> 25 | } 26 | 27 | if (ready && vaults.length === 0) { 28 | return navigate('/')} /> 29 | } 30 | 31 | if (ready && loading) { 32 | return 33 | } 34 | 35 | return ( 36 | 37 | 38 | 39 | <TitleText>Manage balances</TitleText> 40 | <Tooltip 41 | classNames="ml-2 text-lightGray hover:text-white normal-case" 42 | messages={[ 43 | { 44 | title: 'The Universal Vault', 45 | body: UNIVERSAL_VAULT_MSG(), 46 | }, 47 | ]} 48 | /> 49 | 50 | 51 | 52 | 53 | ) 54 | } 55 | 56 | const Title = styled(CardLabel)` 57 | ${tw`p-5 font-normal bg-black text-white`} 58 | ` 59 | 60 | const TitleText = styled.div` 61 | ${tw`text-md uppercase`} 62 | ` 63 | 64 | const Container = styled.div` 65 | ${tw`text-center m-auto flex flex-wrap w-full flex-col`} 66 | ${tw`sm:w-sm`} 67 | ` 68 | -------------------------------------------------------------------------------- /frontend/src/components/VaultsList.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | import copy from 'assets/clipboard.svg' 4 | import { Ellipsis } from 'styling/styles' 5 | import { useContext } from 'react' 6 | import { VaultContext } from 'context/VaultContext' 7 | import { ToolButton } from './ToolButton' 8 | import { Dropdown } from './Dropdown' 9 | 10 | export const VaultsList = () => { 11 | const { vaults, selectVaultById, selectedVault } = useContext(VaultContext) 12 | 13 | const handleCopyToClipboard = () => navigator.clipboard.writeText(selectedVault ? selectedVault.id : vaults[0].id) 14 | const handleVaultChange = (vaultId: string) => selectVaultById(vaultId) 15 | 16 | return ( 17 | <> 18 | {vaults.length > 1 && ( 19 | 20 | 21 | 22 | 23 | Copy 24 | 25 | 26 | {vaults.length > 1 ? ( 27 | v.id)} 29 | selectedOption={selectedVault ? selectedVault.id : vaults[0].id} 30 | onChange={handleVaultChange} 31 | /> 32 | ) : ( 33 | {selectedVault ? selectedVault.id : vaults[0].id} 34 | )} 35 | 36 | )} 37 | 38 | ) 39 | } 40 | 41 | const VaultsListContainer = styled.div` 42 | ${tw`mx-5 my-3`} 43 | ${tw`sm:mx-10 xl:mx-5`} 44 | ` 45 | 46 | const Heading = styled.div` 47 | ${tw`flex flex-row`} 48 | ` 49 | 50 | const Label = styled.span` 51 | ${tw`tracking-wider`} 52 | ` 53 | 54 | const Img = styled.img` 55 | ${tw`w-16px h-16px`} 56 | ${tw`mx-2 fill-current text-link`} 57 | ` 58 | 59 | const SelectedOption = styled.span` 60 | ${Ellipsis} 61 | ${tw`font-bold tracking-wide block my-2 w-336px`} 62 | ` 63 | -------------------------------------------------------------------------------- /frontend/src/components/WelcomeHero.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSpring, animated } from 'react-spring' 3 | import styled from 'styled-components/macro' 4 | import tw from 'twin.macro' 5 | import { safeNumeral } from 'utils/numeral' 6 | import { LoaderDark } from 'styling/styles' 7 | 8 | const WelcomeHero = ({ tvl, totalRewards }) => { 9 | const lockedSpring = useSpring({ 10 | val: tvl, 11 | from: { val: 0 }, 12 | config: { duration: 1500 }, 13 | }) 14 | 15 | const rewardsSpring = useSpring({ 16 | val: totalRewards, 17 | from: { val: 0 }, 18 | config: { duration: 1500 }, 19 | }) 20 | 21 | return ( 22 | 23 | 24 | 25 | Deposit $liquidity-tokens 26 | 27 | 28 | Receive a continuous drip of $reward-tokens 29 | 30 | 31 |
32 | {tvl > 0 || totalRewards > 0 ? ( 33 | 34 | 35 | 36 | {lockedSpring.val.to((val) => `${safeNumeral(val, '$0,0')}`)} 37 | 38 | 39 | 40 | 41 | 42 | {rewardsSpring.val.to((val) => `${safeNumeral(val, '$0,0')}`)} 43 | 44 | 45 | 46 | 47 | ) : ( 48 | 49 | 50 | 51 | )} 52 |
53 | ) 54 | } 55 | 56 | const CalloutBox = styled.div` 57 | ${tw`justify-around flex flex-col w-full`} 58 | ${tw`bg-black text-white p-8 rounded-lg text-md font-mono bg-opacity-90`} 59 | ${tw`mb-10`} 60 | ` 61 | 62 | const StatsBox = styled.div` 63 | ${tw`flex justify-around mt-8`} 64 | ` 65 | 66 | const Stat = styled.div` 67 | ${tw`text-center w-1/2`} 68 | ` 69 | 70 | const StatValue = styled.div` 71 | ${tw`text-4xl font-bold font-mono`} 72 | ` 73 | 74 | const Label = styled.div` 75 | ${tw`mt-2 text-sm text-gray font-bold`} 76 | ` 77 | 78 | const List = styled.ul` 79 | ${tw`p-0 mx-10 mb-5`} 80 | ` 81 | 82 | const ListItem = styled.li` 83 | ${tw`flex mb-2`} 84 | list-style-type: disc; 85 | display: list-item; 86 | ` 87 | 88 | const RedHighlight = styled.span` 89 | ${tw`text-primary font-bold`} 90 | ` 91 | 92 | const GreenHighlight = styled.span` 93 | ${tw`text-greenLight font-bold`} 94 | ` 95 | 96 | const Hr = styled.hr` 97 | ${tw`border-t border-white w-full`} 98 | ` 99 | const LoaderContainer = styled.div` 100 | ${tw`flex items-center justify-center w-full mt-11 mb-10`} 101 | ` 102 | 103 | export default WelcomeHero 104 | -------------------------------------------------------------------------------- /frontend/src/components/WelcomeMessage.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | 4 | export const WelcomeMessage = () => ( 5 | 6 | 7 | Stake <Highlight>Liquidity</Highlight> 8 | 9 | 10 | 14 | Geysers 15 | {' '} 16 | are smart faucets that incentivize  17 | 18 | AMPL 19 | {' '} 20 | and{' '} 21 | 22 | SPOT 23 | {' '} 24 | on-chain liquidity. 25 |
26 | The more liquidity you provide and for longer, the more rewards you receive. 27 |
28 |
29 | ) 30 | 31 | const Container = styled.div` 32 | ${tw`w-full my-8 px-4`} 33 | ` 34 | 35 | const Title = styled.h1` 36 | ${tw`text-2xl font-bold mb-4 font-roboto`} 37 | ` 38 | 39 | const Highlight = styled.span` 40 | ${tw`text-black font-regular`} 41 | ` 42 | 43 | const Subtitle = styled.p` 44 | ${tw`text-base text-black mb-6 leading-relaxed`} 45 | ` 46 | 47 | const Link = styled.a` 48 | ${tw`cursor-pointer text-link hover:underline`} 49 | ` 50 | -------------------------------------------------------------------------------- /frontend/src/context/AppContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState } from 'react' 2 | import { Mode } from '../constants' 3 | 4 | export const AppContext = createContext<{ 5 | mode: Mode 6 | toggleMode: () => void 7 | }>({ 8 | mode: Mode.GEYSERS, 9 | toggleMode: () => {}, 10 | }) 11 | 12 | export const AppContextProvider: React.FC = ({ children }) => { 13 | const [appMode, setAppMode] = useState(Mode.GEYSERS) 14 | 15 | const toggleMode = () => setAppMode(appMode === Mode.GEYSERS ? Mode.VAULTS : Mode.GEYSERS) 16 | 17 | return {children} 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/context/SubgraphContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react' 2 | import { ApolloProvider } from '@apollo/client' 3 | import Web3Context from 'context/Web3Context' 4 | import { makeClient } from 'queries/client' 5 | 6 | const SubgraphContext = createContext<{}>({}) 7 | 8 | export type SubgraphContextProps = { 9 | children?: React.ReactNode 10 | } 11 | 12 | const defaultProps: SubgraphContextProps = { 13 | children: null, 14 | } 15 | 16 | const SubgraphProvider: React.FC = ({ children }: SubgraphContextProps) => { 17 | const { networkId } = useContext(Web3Context) 18 | const client = makeClient(networkId) 19 | return {children} 20 | } 21 | 22 | SubgraphProvider.defaultProps = defaultProps 23 | 24 | export { SubgraphProvider } 25 | 26 | export default SubgraphContext 27 | -------------------------------------------------------------------------------- /frontend/src/context/WalletContext.tsx: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | import { createContext, useCallback, useContext, useEffect, useState } from 'react' 3 | import { ERC20Balance } from '../sdk' 4 | import { GeyserContext } from './GeyserContext' 5 | import Web3Context from './Web3Context' 6 | import { TokenInfo } from '../types' 7 | 8 | export const WalletContext = createContext<{ 9 | stakingTokenBalance: BigNumber 10 | underlyingTokenBalance: BigNumber 11 | refreshWalletBalances: () => void 12 | }>({ 13 | stakingTokenBalance: BigNumber.from('0'), 14 | underlyingTokenBalance: BigNumber.from('0'), 15 | refreshWalletBalances: () => {}, 16 | }) 17 | 18 | export const WalletContextProvider: React.FC = ({ children }) => { 19 | const [stakingTokenBalance, setStakingTokenBalance] = useState(BigNumber.from('0')) 20 | const [underlyingTokenBalance, setWrappedTokenBalance] = useState(BigNumber.from('0')) 21 | 22 | const { signer } = useContext(Web3Context) 23 | const { 24 | selectedGeyserInfo: { stakingTokenInfo, isWrapped }, 25 | } = useContext(GeyserContext) 26 | const underlyingStakingTokenInfo = stakingTokenInfo.wrappedToken as TokenInfo 27 | 28 | const getStakingTokenBalance = useCallback(async () => { 29 | if (stakingTokenInfo && stakingTokenInfo.address && signer) { 30 | try { 31 | const balance = await ERC20Balance(stakingTokenInfo.address, await signer.getAddress(), signer) 32 | return balance 33 | } catch (e) { 34 | console.log('wallet balance query error') 35 | // console.error(e) 36 | return BigNumber.from('0') 37 | } 38 | } 39 | return BigNumber.from('0') 40 | }, [stakingTokenInfo?.address, signer]) 41 | 42 | const getWrappedTokenBalance = useCallback(async () => { 43 | if (isWrapped && underlyingStakingTokenInfo && underlyingStakingTokenInfo.address && signer) { 44 | try { 45 | const balance = await ERC20Balance(underlyingStakingTokenInfo.address, await signer.getAddress(), signer) 46 | return balance 47 | } catch (e) { 48 | console.log('wallet balance query error') 49 | // console.error(e) 50 | return BigNumber.from('0') 51 | } 52 | } 53 | return BigNumber.from('0') 54 | }, [underlyingStakingTokenInfo?.address, signer]) 55 | 56 | const refreshWalletBalances = async () => { 57 | setStakingTokenBalance(await getStakingTokenBalance()) 58 | setWrappedTokenBalance(await getWrappedTokenBalance()) 59 | } 60 | 61 | useEffect(() => { 62 | let mounted = true 63 | setTimeout(() => mounted && refreshWalletBalances(), 250) 64 | return () => { 65 | mounted = false 66 | } 67 | }, [getStakingTokenBalance, getWrappedTokenBalance]) 68 | 69 | return ( 70 | 71 | {children} 72 | 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /frontend/src/hooks/useTxStateMachine.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { TransactionResponse, TransactionReceipt } from '@ethersproject/providers' 3 | import { TxState } from '../constants' 4 | 5 | type SubmitFunction = (receipt?: TransactionReceipt) => Promise 6 | 7 | type CurrentTxState = { 8 | state: TxState 9 | response?: TransactionResponse 10 | receipt?: TransactionReceipt 11 | } 12 | 13 | export type TxStateMachine = CurrentTxState & { 14 | submitTx: (receipt?: TransactionReceipt) => Promise 15 | refresh: () => void 16 | } 17 | 18 | export const useTxStateMachine = (submit: SubmitFunction) => { 19 | const [currentTxState, setCurrentTxState] = useState({ state: TxState.PENDING }) 20 | 21 | useEffect(() => { 22 | ;(async () => { 23 | const { response } = currentTxState 24 | try { 25 | if (response) { 26 | const receipt = await response.wait(1) 27 | setCurrentTxState((txState) => ({ ...txState, receipt, state: TxState.MINED })) 28 | } 29 | } catch (e) { 30 | setCurrentTxState((txState) => ({ ...txState, state: TxState.FAILED })) 31 | } 32 | })() 33 | }, [currentTxState.response]) 34 | 35 | const submitTx = async (receipt?: TransactionReceipt) => { 36 | try { 37 | const response = await submit(receipt) 38 | if (response) { 39 | setCurrentTxState((txState) => ({ ...txState, response, state: TxState.SUBMITTED })) 40 | } else { 41 | setCurrentTxState((txState) => ({ ...txState, state: TxState.FAILED })) 42 | } 43 | } catch (e) { 44 | setCurrentTxState((txState) => ({ ...txState, state: TxState.FAILED })) 45 | } 46 | } 47 | 48 | const refresh = () => { 49 | setCurrentTxState({ state: TxState.PENDING }) 50 | } 51 | 52 | return { 53 | submitTx, 54 | refresh, 55 | ...currentTxState, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | min-width: 620px; 7 | min-height: 1100px; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | background-color: #fff; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | import reportWebVitals from './reportWebVitals' 6 | 7 | /// NOTE: BTOA monkey-patch. Required for coinbase wallet. 8 | const originalBtoa = window.btoa 9 | window.btoa = (str) => { 10 | try { 11 | return originalBtoa(str) 12 | } catch (e) { 13 | return originalBtoa(unescape(encodeURIComponent(str))) 14 | } 15 | } 16 | 17 | ReactDOM.render( 18 | 19 | 20 | , 21 | document.getElementById('root'), 22 | ) 23 | 24 | // If you want to start measuring performance in your app, pass a function 25 | // to log results (for example: reportWebVitals(console.log)) 26 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 27 | reportWebVitals() 28 | -------------------------------------------------------------------------------- /frontend/src/queries/client.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache, DefaultOptions } from '@apollo/client' 2 | import { getConnectionConfig } from 'config/app' 3 | 4 | export const makeClient = (networkId: number | null) => { 5 | const { graphUrl } = getConnectionConfig(networkId) 6 | 7 | const defaultOptions: DefaultOptions = { 8 | watchQuery: { 9 | fetchPolicy: 'cache-first', 10 | }, 11 | query: { 12 | fetchPolicy: 'cache-first', 13 | }, 14 | mutate: { 15 | fetchPolicy: 'no-cache', 16 | }, 17 | } 18 | 19 | return new ApolloClient({ 20 | uri: graphUrl, 21 | cache: new InMemoryCache(), 22 | defaultOptions, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/queries/geyser.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | export const GET_GEYSERS = gql` 4 | query getGeysers($ids: [ID!]!) { 5 | geysers(first: 1000, where: { id_in: $ids }) { 6 | id 7 | rewardToken 8 | stakingToken 9 | totalStake 10 | totalStakeUnits 11 | scalingFloor 12 | scalingCeiling 13 | scalingTime 14 | unlockedReward 15 | rewardBalance 16 | bonusTokens 17 | rewardPool 18 | rewardPoolBalances(first: 1000) { 19 | id 20 | token 21 | balance 22 | } 23 | rewardSchedules(first: 1000) { 24 | id 25 | duration 26 | start 27 | rewardAmount 28 | } 29 | lastUpdate 30 | powerSwitch { 31 | id 32 | status 33 | } 34 | } 35 | } 36 | ` 37 | -------------------------------------------------------------------------------- /frontend/src/queries/vault.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | export const GET_USER_VAULTS = gql` 4 | query getUserVaults($id: ID!) { 5 | user(id: $id) { 6 | vaults(first: 1000) { 7 | id 8 | nonce 9 | claimedReward(first: 1000) { 10 | id 11 | token 12 | amount 13 | lastUpdate 14 | } 15 | locks(first: 1000, where: { amount_gt: 0 }) { 16 | id 17 | token 18 | amount 19 | stakeUnits 20 | lastUpdate 21 | geyser { 22 | id 23 | } 24 | } 25 | } 26 | } 27 | } 28 | ` 29 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals' 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry) 7 | getFID(onPerfEntry) 8 | getFCP(onPerfEntry) 9 | getLCP(onPerfEntry) 10 | getTTFB(onPerfEntry) 11 | }) 12 | } 13 | } 14 | 15 | export default reportWebVitals 16 | -------------------------------------------------------------------------------- /frontend/src/sdk/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions' 2 | export * from './utils' 3 | export * from './tokens' 4 | -------------------------------------------------------------------------------- /frontend/src/sdk/stats.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish, Contract, providers, Signer } from 'ethers' 2 | import { TransactionReceipt } from '@ethersproject/providers' 3 | import { loadNetworkConfig, parseAllEventsFromReceipt } from './utils' 4 | import { VaultData } from './types' 5 | 6 | async function _execGeyserFunction( 7 | geyserAddress: string, 8 | signerOrProvider: Signer | providers.Provider, 9 | fnc: string, 10 | args: any[] = [], 11 | ): Promise { 12 | const config = await loadNetworkConfig(signerOrProvider) 13 | const geyser = new Contract(geyserAddress, config.GeyserTemplate.abi, signerOrProvider) 14 | return geyser[fnc](...args) as Promise 15 | } 16 | 17 | async function _execVaultFunction( 18 | vaultAddress: string, 19 | signerOrProvider: Signer | providers.Provider, 20 | fnc: string, 21 | args: any[] = [], 22 | ): Promise { 23 | const config = await loadNetworkConfig(signerOrProvider) 24 | const vault = new Contract(vaultAddress, config.VaultTemplate.abi, signerOrProvider) 25 | return vault[fnc](...args) as Promise 26 | } 27 | 28 | export const getGeyserVaultData = async ( 29 | geyserAddress: string, 30 | vaultAddress: string, 31 | signerOrProvider: Signer | providers.Provider, 32 | ) => { 33 | return _execGeyserFunction(geyserAddress, signerOrProvider, 'getVaultData', [vaultAddress]) 34 | } 35 | 36 | export const getCurrentVaultReward = async ( 37 | vaultAddress: string, 38 | geyserAddress: string, 39 | signerOrProvider: Signer | providers.Provider, 40 | ) => { 41 | return _execGeyserFunction(geyserAddress, signerOrProvider, 'getCurrentVaultReward', [vaultAddress]) 42 | } 43 | 44 | export const getFutureVaultReward = async ( 45 | vaultAddress: string, 46 | geyserAddress: string, 47 | timestamp: number, 48 | signerOrProvider: Signer | providers.Provider, 49 | ) => { 50 | return _execGeyserFunction(geyserAddress, signerOrProvider, 'getFutureVaultReward', [ 51 | vaultAddress, 52 | timestamp, 53 | ]) 54 | } 55 | 56 | export const getCurrentUnlockedRewards = async ( 57 | geyserAddress: string, 58 | signerOrProvider: Signer | providers.Provider, 59 | ) => { 60 | return _execGeyserFunction(geyserAddress, signerOrProvider, 'getCurrentUnlockedRewards') 61 | } 62 | 63 | export const getFutureUnlockedRewards = async ( 64 | geyserAddress: string, 65 | timestamp: number, 66 | signerOrProvider: Signer | providers.Provider, 67 | ) => { 68 | return _execGeyserFunction(geyserAddress, signerOrProvider, 'getFutureUnlockedRewards', [timestamp]) 69 | } 70 | 71 | export const getCurrentStakeReward = async ( 72 | vaultAddress: string, 73 | geyserAddress: string, 74 | amount: BigNumberish, 75 | signerOrProvider: Signer | providers.Provider, 76 | ) => { 77 | return _execGeyserFunction(geyserAddress, signerOrProvider, 'getCurrentStakeReward', [ 78 | vaultAddress, 79 | amount, 80 | ]) 81 | } 82 | 83 | export const getBalanceLocked = async ( 84 | vaultAddress: string, 85 | tokenAddress: string, 86 | signerOrProvider: Signer | providers.Provider, 87 | ) => { 88 | return _execVaultFunction(vaultAddress, signerOrProvider, 'getBalanceLocked', [tokenAddress]) 89 | } 90 | 91 | export const getRewardsClaimedFromUnstake = async ( 92 | receipt: TransactionReceipt, 93 | geyserAddress: string, 94 | signerOrProvider: Signer | providers.Provider, 95 | ) => { 96 | const config = await loadNetworkConfig(signerOrProvider) 97 | const geyser = new Contract(geyserAddress, config.GeyserTemplate.abi, signerOrProvider) 98 | const eventLogs = parseAllEventsFromReceipt(receipt, geyser, 'RewardClaimed') 99 | if (eventLogs.length === 0) return null 100 | 101 | const { rewardToken } = await geyser.getGeyserData() 102 | const rewardTokenLog = eventLogs.filter((l) => l.args.token === rewardToken) 103 | return rewardTokenLog.length > 0 ? rewardTokenLog[0].args : null 104 | } 105 | -------------------------------------------------------------------------------- /frontend/src/sdk/tokens.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, Contract, providers, Signer } from 'ethers' 2 | import { ERC20_ABI } from './abis' 3 | 4 | function _execTokenFunction( 5 | tokenAddress: string, 6 | signerOrProvider: Signer | providers.Provider, 7 | fnc: string, 8 | args: any[] = [], 9 | ): Promise { 10 | const token = new Contract(tokenAddress, ERC20_ABI, signerOrProvider) 11 | return token[fnc](...args) as Promise 12 | } 13 | 14 | export const ERC20Decimals = async (tokenAddress: string, signerOrProvider: Signer | providers.Provider) => { 15 | return _execTokenFunction(tokenAddress, signerOrProvider, 'decimals') 16 | } 17 | 18 | export const ERC20Name = async (tokenAddress: string, signerOrProvider: Signer | providers.Provider) => { 19 | return _execTokenFunction(tokenAddress, signerOrProvider, 'name') 20 | } 21 | 22 | export const ERC20Symbol = async (tokenAddress: string, signerOrProvider: Signer | providers.Provider) => { 23 | return _execTokenFunction(tokenAddress, signerOrProvider, 'symbol') 24 | } 25 | 26 | export const ERC20Balance = async ( 27 | tokenAddress: string, 28 | holderAddress: string, 29 | signerOrProvider: Signer | providers.Provider, 30 | ) => { 31 | return _execTokenFunction(tokenAddress, signerOrProvider, 'balanceOf', [holderAddress]) 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/sdk/types.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | 3 | export type VaultData = { 4 | totalStake: BigNumber 5 | stakes: UserStake[] 6 | } 7 | 8 | export type UserStake = { 9 | timestamp: BigNumber 10 | amount: BigNumber 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/styling/colors.ts: -------------------------------------------------------------------------------- 1 | // https://chir.ag/projects/name-that-color/#2D4A5D 2 | export enum NamedColors { 3 | ALTO = '#DDDDDD', 4 | AMARANTH = '#EE2A4F', 5 | APPLE = '#2ECC40', 6 | BLACK = '#000000', 7 | ELECTRICAL_VIOLET = '#912DFF', 8 | GRAY = '#808080', 9 | RADICAL_RED = '#FF2D55', 10 | RED_ORANGE = '#FF4136', 11 | SCHOOL_BUS_YELLOW = '#FFDC00', 12 | WHITE = '#FFFFFF', 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/styling/mixins.ts: -------------------------------------------------------------------------------- 1 | // Collective place for unsiversal mixins 2 | import { css } from 'styled-components/macro' 3 | 4 | export const PaddedDiv = (padding: string) => css` 5 | padding: ${padding}; 6 | ` 7 | 8 | export const Aligned = (alignment: string) => css` 9 | text-align: ${alignment}; 10 | ` 11 | -------------------------------------------------------------------------------- /frontend/src/styling/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components/macro' 2 | import tw from 'twin.macro' 3 | import { Button } from 'components/Button' 4 | 5 | export const Paragraph = styled.p` 6 | color: ${(props) => props.color}; 7 | ${tw`text-base font-bold m-auto`} 8 | ` 9 | 10 | export const Overlay = styled.div` 11 | ${tw`shadow-all w-full rounded-lg my-2`} 12 | ${tw`sm:my-4`} 13 | ` 14 | 15 | export const ResponsiveHeader = css` 16 | ${tw`text-base sm:text-lg`} 17 | ` 18 | 19 | export const ResponsiveText = css` 20 | ${tw`text-sm sm:text-base`} 21 | ` 22 | 23 | export const ResponsiveSubText = css` 24 | ${tw`text-xs sm:text-xs`} 25 | ` 26 | 27 | export const Centered = styled.div` 28 | ${tw`h-full w-full m-auto self-center`} 29 | ` 30 | 31 | export const Ellipsis = css` 32 | ${tw`overflow-ellipsis overflow-hidden`} 33 | ` 34 | 35 | // typography 36 | 37 | export const CardLabel = styled.span` 38 | ${tw`flex capitalize text-gray font-light`} 39 | ` 40 | 41 | export const CardValue = styled.span` 42 | ${tw`flex flex-wrap text-base whitespace-pre-wrap`} 43 | ` 44 | 45 | export const ModalButton = styled(Button)` 46 | width: 40%; 47 | ${tw`inline-flex items-center justify-center px-4 py-2 text-sm font-medium border rounded-md`} 48 | ${tw`focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2`} 49 | ` 50 | 51 | // loader 52 | 53 | export const Loader = styled.div` 54 | height: 10px; 55 | aspect-ratio: 6; 56 | --c: #0000 64%, #000 66% 98%, #0000 101%; 57 | background: radial-gradient(35% 146% at 50% 159%, var(--c)) 0 0, 58 | radial-gradient(35% 146% at 50% -59%, var(--c)) 25% 100%; 59 | background-size: calc(100% / 3) 50%; 60 | background-repeat: repeat-x; 61 | clip-path: inset(0 100% 0 0); 62 | animation: l2 1s infinite linear; 63 | @keyframes l2 { 64 | 90%, 65 | to { 66 | clip-path: inset(0); 67 | } 68 | } 69 | ` 70 | 71 | export const LoaderDark = styled.div` 72 | height: 10px; 73 | aspect-ratio: 6; 74 | --c: transparent 64%, white 66% 98%, transparent 101%; 75 | background: radial-gradient(35% 146% at 50% 159%, var(--c)) 0 0, 76 | radial-gradient(35% 146% at 50% -59%, var(--c)) 25% 100%; 77 | background-size: calc(100% / 3) 50%; 78 | background-repeat: repeat-x; 79 | clip-path: inset(0 100% 0 0); 80 | animation: l2 1s infinite linear; 81 | @keyframes l2 { 82 | 90%, 83 | to { 84 | clip-path: inset(0); 85 | } 86 | } 87 | ` 88 | -------------------------------------------------------------------------------- /frontend/src/utils/amount.ts: -------------------------------------------------------------------------------- 1 | import { BigNumberish } from 'ethers' 2 | import { formatUnits } from 'ethers/lib/utils' 3 | import { safeNumeral } from './numeral' 4 | 5 | export const amountOrZero = (amount?: BigNumberish) => amount || '0' 6 | 7 | export const formatAmount = (amount: BigNumberish, decimals: number) => 8 | safeNumeral(parseFloat(formatUnits(amount, decimals)), '0.000') 9 | 10 | function toSubscript(num) { 11 | const subscriptMap = { 12 | '0': '₀', 13 | '1': '₁', 14 | '2': '₂', 15 | '3': '₃', 16 | '4': '₄', 17 | '5': '₅', 18 | '6': '₆', 19 | '7': '₇', 20 | '8': '₈', 21 | '9': '₉', 22 | } 23 | return String(num) 24 | .split('') 25 | .map((char) => subscriptMap[char] || char) 26 | .join('') 27 | } 28 | 29 | export function formatTokenBalance(balance, defaultFormat = '0,0.000', precision = 1000, largeFormat = '0.000a') { 30 | const num = typeof balance === 'string' ? parseFloat(balance) : balance 31 | if (num < 1 / precision && num !== 0) { 32 | const numStr = num.toFixed(20).replace(/0+$/, '') 33 | const [, decimalPart = ''] = numStr.split('.') 34 | const leadingZeros = decimalPart.match(/^0+/)?.[0]?.length || 0 35 | const significantDigits = decimalPart.slice(leadingZeros) 36 | const subscriptZeros = toSubscript(leadingZeros) 37 | return `0.0${subscriptZeros}${significantDigits}` 38 | } else if (num > 1000000) { 39 | return safeNumeral(num, largeFormat) 40 | } 41 | return safeNumeral(num, defaultFormat) 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/utils/ampleforth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getAmpleforthPolicy, 3 | getRebases, 4 | getXCAmpleController, 5 | getXCRebases, 6 | queries, 7 | entities, 8 | } from '@ampleforthorg/sdk' 9 | import { formatUnits } from 'ethers/lib/utils' 10 | import { Signer, providers } from 'ethers' 11 | import { RewardSchedule, SignerOrProvider } from '../types' 12 | import * as ls from './cache' 13 | import { HOUR_IN_MS, DAY_IN_MS } from '../constants' 14 | 15 | const getClient = (chainId) => queries.initializeClient(queries.graphHostedURL(chainId)) 16 | 17 | const apiEndpoint = 'https://web-api.ampleforth.org' 18 | export const loadStakeAPYsFromCache = async () => 19 | ls.computeAndCache>>( 20 | async () => { 21 | const response = await fetch(`${apiEndpoint}/staking/apy`) 22 | const data = await response.json() 23 | return data 24 | }, 25 | 'LPAPYS', 26 | HOUR_IN_MS, 27 | ) 28 | 29 | const loadXCRebasesFromCache = async (controller: entities.XCController, chainId: number) => 30 | ls.computeAndCache( 31 | async () => (await getXCRebases(controller, chainId, getClient(chainId))).map((r) => r.rawData), 32 | `${controller.address}|xc_rebases|${controller.epoch.toString()}`, 33 | DAY_IN_MS, 34 | ) 35 | 36 | const loadRebasesFromCache = async (policy: entities.Policy, chainId: number) => 37 | ls.computeAndCache( 38 | async () => (await getRebases(policy, chainId, getClient(chainId))).map((r) => r.rawData), 39 | `${policy.address}|rebases|${policy.epoch.toString()}`, 40 | DAY_IN_MS, 41 | ) 42 | 43 | export const computeAMPLRewardShares = async ( 44 | rewardSchedules: RewardSchedule[], 45 | tokenAddress: string, 46 | policyAddress: string, 47 | isCrossChain: boolean, 48 | epoch: number, 49 | decimals: number, 50 | signerOrProvider: SignerOrProvider, 51 | ) => { 52 | const provider = (signerOrProvider as Signer).provider || (signerOrProvider as providers.Provider) 53 | // console.log("provider present", !!provider.network) 54 | const { chainId } = provider.network || (await provider.getNetwork()) 55 | 56 | if (isCrossChain) { 57 | const controller = await getXCAmpleController(chainId, getClient(chainId)) 58 | const rebases = await loadXCRebasesFromCache(controller, chainId, getClient(chainId)) 59 | controller.loadHistoricalRebases(rebases.map((r) => new entities.XCRebase(r))) 60 | // const rebases = await getXCRebases(controller, chainId, getClient(chainId)) 61 | // controller.loadHistoricalRebases(rebases) 62 | const getShares = (schedule: RewardSchedule) => 63 | parseFloat(formatUnits(schedule.rewardAmount, decimals)) / controller.getSupplyOn(schedule.start).toNumber() 64 | return rewardSchedules.reduce((acc, schedule) => acc + getShares(schedule), 0) 65 | } 66 | 67 | const policy = await getAmpleforthPolicy(chainId, getClient(chainId)) 68 | const rebases = await loadRebasesFromCache(policy, chainId, getClient(chainId)) 69 | policy.loadHistoricalRebases(rebases.map((r) => new entities.Rebase(r))) 70 | // const rebases = await getRebases(policy, chainId, getClient(chainId)) 71 | // policy.loadHistoricalRebases(rebases) 72 | const getShares = (schedule: RewardSchedule) => 73 | parseFloat(formatUnits(schedule.rewardAmount, decimals)) / policy.getSupplyOn(schedule.start).toNumber() 74 | return rewardSchedules.reduce((acc, schedule) => acc + getShares(schedule), 0) 75 | } 76 | -------------------------------------------------------------------------------- /frontend/src/utils/bonusToken.ts: -------------------------------------------------------------------------------- 1 | import { BonusTokenInfo, SignerOrProvider } from '../types' 2 | import { defaultTokenInfo, getTokenInfo } from './token' 3 | import { getCurrentPrice } from './price' 4 | 5 | export const defaultBonusTokenInfo = (): BonusTokenInfo => ({ 6 | ...defaultTokenInfo(), 7 | price: 0, 8 | }) 9 | 10 | export const getBonusTokenInfo = async (tokenAddress: string, signerOrProvider: SignerOrProvider) => 11 | getBasicToken(tokenAddress, signerOrProvider) 12 | 13 | const getBasicToken = async (tokenAddress: string, signerOrProvider: SignerOrProvider): Promise => { 14 | const tokenInfo = await getTokenInfo(tokenAddress, signerOrProvider) 15 | const price = await getCurrentPrice(tokenInfo.symbol) 16 | return { 17 | ...tokenInfo, 18 | price, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/utils/cache.ts: -------------------------------------------------------------------------------- 1 | export const set = (key: string, value: any, ttl: number) => { 2 | const data = { value, expiresAt: new Date().getTime() + ttl } 3 | localStorage.setItem(key, JSON.stringify(data)) 4 | } 5 | 6 | export const get = (key: string): any => { 7 | const data = localStorage.getItem(key) 8 | // console.log("cache hit", !!data, key) 9 | if (data !== null) { 10 | const { value, expiresAt } = JSON.parse(data) 11 | if (expiresAt && expiresAt < new Date().getTime()) { 12 | localStorage.removeItem(key) 13 | } else { 14 | return value 15 | } 16 | } 17 | return null 18 | } 19 | 20 | export async function computeAndCache( 21 | getValueOperation: () => Promise, 22 | key: string, 23 | ttl: number, 24 | useCache: (cached: T) => boolean = () => true, 25 | ): Promise { 26 | const cachedValue = get(key) 27 | if (cachedValue && useCache(cachedValue)) return cachedValue 28 | const value = await getValueOperation() 29 | set(key, value, ttl) 30 | return value 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/utils/eth.ts: -------------------------------------------------------------------------------- 1 | import { Contract, providers, Signer } from 'ethers' 2 | import { SignerOrProvider } from '../types' 3 | 4 | export const loadHistoricalLogs = async ( 5 | contract: Contract, 6 | eventName: string, 7 | signerOrProvider: SignerOrProvider, 8 | startBlock = 0, 9 | BLOCKS_PER_PART = 2102400 / 2.5, 10 | ) => { 11 | const signer = signerOrProvider as Signer 12 | const provider = signerOrProvider as providers.Provider 13 | const ethersProvider = signer.provider || provider 14 | const endBlock = await ethersProvider.getBlockNumber() 15 | const filter = contract.filters[eventName]() 16 | let logs: any[] = [] 17 | for (let i = startBlock; i <= endBlock; i += BLOCKS_PER_PART) { 18 | const partEnd = Math.min(i + BLOCKS_PER_PART, endBlock) 19 | const partLogs = await contract.queryFilter(filter, i, partEnd) 20 | logs = logs.concat(partLogs) 21 | // console.log('Loading rebase logs', i, endBlock, partLogs.length) 22 | } 23 | return logs 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/utils/formatDisplayAddress.ts: -------------------------------------------------------------------------------- 1 | import { toChecksumAddress } from 'web3-utils' 2 | 3 | const formatAddr = (addr: string) => `${addr.slice(0, 6)}...${addr.slice(addr.length - 4)}` 4 | export const displayAddr = (addr: string) => formatAddr(toChecksumAddress(addr)) 5 | -------------------------------------------------------------------------------- /frontend/src/utils/numeral.ts: -------------------------------------------------------------------------------- 1 | import numeral from 'numeral-es6' 2 | import { DAY_IN_SEC, HOUR_IN_SEC, MIN_IN_SEC, MONTH_IN_SEC, WEEK_IN_SEC } from '../constants' 3 | 4 | export const safeNumeral = (n: number, f: string): string => { 5 | const safeNum: string = numeral(n).format(f) 6 | return safeNum === 'NaN' ? numeral(0).format(f) : safeNum 7 | } 8 | 9 | export const formatWithDecimals = (value: string, decimals = 2) => { 10 | if (decimals === 0) return value 11 | const parts = value.split('.') 12 | if (parts.length > 1) { 13 | if (parts[1].length >= decimals) return value 14 | const missingDecimals = decimals - parts[1].length 15 | return `${value}${Array(missingDecimals).fill('0').join('')}` 16 | } 17 | return `${value}.${Array(decimals).fill('0').join('')}` 18 | } 19 | 20 | export const humanReadableDuration = (duration: number) => { 21 | const durationLabel = [ 22 | { 23 | duration: MONTH_IN_SEC, 24 | label: 'month', 25 | }, 26 | { 27 | duration: WEEK_IN_SEC, 28 | label: 'week', 29 | }, 30 | { 31 | duration: DAY_IN_SEC, 32 | label: 'day', 33 | }, 34 | { 35 | duration: HOUR_IN_SEC, 36 | label: 'hour', 37 | }, 38 | { 39 | duration: MIN_IN_SEC, 40 | label: 'minute', 41 | }, 42 | { 43 | duration: 1, 44 | label: 'second', 45 | }, 46 | ] 47 | 48 | const index = 49 | (durationLabel.findIndex(({ duration: d }) => duration >= d) + durationLabel.length) % durationLabel.length 50 | const { duration: d, label } = durationLabel[index] 51 | const n = duration / d 52 | return { 53 | numeral: safeNumeral(n, '0'), 54 | units: `${label}${n > 1 ? 's' : ''}`, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/utils/price.ts: -------------------------------------------------------------------------------- 1 | import { MIN_IN_MS } from '../constants' 2 | import * as ls from './cache' 3 | 4 | const DEFAULT_PRICES: Record = { 5 | AMPL: 1.19, 6 | WAMPL: 20, 7 | BTC: 75000.0, 8 | WETH: 3000, 9 | ETH: 2000, 10 | WAVAX: 20, 11 | USDC: 1, 12 | SPOT: 1.3, 13 | FORTH: 4, 14 | } 15 | 16 | const symbolReMap: Record = { 17 | WETH: 'ETH', 18 | } 19 | 20 | const URL = 'https://web-api.ampleforth.org/util/get-price' 21 | export const getCurrentPrice = async (symbol_: string) => { 22 | let symbol = symbol_.toUpperCase() 23 | symbol = symbolReMap[symbol] || symbol 24 | const cacheKey = `geyser|${symbol}|spot` 25 | const TTL = 15 * MIN_IN_MS 26 | try { 27 | return await ls.computeAndCache( 28 | async () => { 29 | const response = await fetch(`${URL}?symbol=${symbol}`) 30 | const price = await response.json() 31 | return price as number 32 | }, 33 | cacheKey, 34 | TTL, 35 | ) 36 | } catch (e) { 37 | console.error(e) 38 | return DEFAULT_PRICES[symbol] || 0 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/utils/rewardToken.ts: -------------------------------------------------------------------------------- 1 | import { RewardToken, MIN_IN_MS } from '../constants' 2 | import { RewardTokenInfo, SignerOrProvider } from '../types' 3 | import { defaultTokenInfo, getTokenInfo } from './token' 4 | import { getCurrentPrice } from './price' 5 | import * as ls from './cache' 6 | 7 | const cacheTimeMs = 30 * MIN_IN_MS 8 | 9 | export const defaultRewardTokenInfo = (): RewardTokenInfo => ({ 10 | ...defaultTokenInfo(), 11 | price: 1, 12 | }) 13 | 14 | export const getRewardTokenInfo = async ( 15 | tokenAddress: string, 16 | token: RewardToken, 17 | signerOrProvider: SignerOrProvider, 18 | ) => { 19 | switch (token) { 20 | case RewardToken.MOCK: 21 | return getBasicToken(tokenAddress, signerOrProvider) 22 | case RewardToken.AMPL: 23 | return getAMPLToken(tokenAddress, signerOrProvider) 24 | case RewardToken.XCAMPLE: 25 | return getAMPLToken(tokenAddress, signerOrProvider) 26 | case RewardToken.WAMPL: 27 | return getBasicToken(tokenAddress, signerOrProvider) 28 | case RewardToken.SPOT: 29 | return getBasicToken(tokenAddress, signerOrProvider) 30 | case RewardToken.FORTH: 31 | return getBasicToken(tokenAddress, signerOrProvider) 32 | default: 33 | throw new Error(`Handler for ${token} not found`) 34 | } 35 | } 36 | 37 | const getBasicToken = async (tokenAddress: string, signerOrProvider: SignerOrProvider): Promise => 38 | ls.computeAndCache( 39 | async function () { 40 | const tokenInfo = await getTokenInfo(tokenAddress, signerOrProvider) 41 | const price = await getCurrentPrice(tokenInfo.symbol) 42 | return { price, ...tokenInfo } 43 | }, 44 | `rewardTokenInfo:${tokenAddress}`, 45 | cacheTimeMs, 46 | ) 47 | 48 | const getAMPLToken = async (tokenAddress: string, signerOrProvider: SignerOrProvider): Promise => 49 | ls.computeAndCache( 50 | async function () { 51 | const tokenInfo = await getTokenInfo(tokenAddress, signerOrProvider) 52 | const price = await getCurrentPrice('AMPL') 53 | return { price, ...tokenInfo } 54 | }, 55 | `rewardTokenInfo:${tokenAddress}`, 56 | cacheTimeMs, 57 | ) 58 | -------------------------------------------------------------------------------- /frontend/src/utils/token.ts: -------------------------------------------------------------------------------- 1 | import { toChecksumAddress } from 'web3-utils' 2 | import { ERC20Decimals, ERC20Name, ERC20Symbol } from '../sdk' 3 | import { SignerOrProvider, TokenInfo } from '../types' 4 | import * as ls from './cache' 5 | import { CONST_CACHE_TIME_MS } from '../constants' 6 | 7 | export const getTokenInfo = async ( 8 | tokenAddress: string, 9 | signerOrProvider: SignerOrProvider, 10 | ttl: number = CONST_CACHE_TIME_MS, 11 | ): Promise => { 12 | const address = toChecksumAddress(tokenAddress) 13 | return ls.computeAndCache( 14 | async () => { 15 | const value: TokenInfo = { 16 | address, 17 | name: await ERC20Name(address, signerOrProvider), 18 | symbol: await ERC20Symbol(address, signerOrProvider), 19 | decimals: await ERC20Decimals(address, signerOrProvider), 20 | } 21 | return value 22 | }, 23 | `${address}|tokenInfo`, 24 | ttl, 25 | ) 26 | } 27 | 28 | export const defaultTokenInfo = (): TokenInfo => ({ 29 | address: '', 30 | name: '', 31 | symbol: '', 32 | decimals: 0, 33 | }) 34 | -------------------------------------------------------------------------------- /frontend/src/utils/wrap.ts: -------------------------------------------------------------------------------- 1 | import { TransactionResponse } from '@ethersproject/providers' 2 | import { BigNumberish, Contract, Signer } from 'ethers' 3 | import { ERC20_ABI } from '../sdk/abis' 4 | import { WRAPPED_ERC20_ABI } from './abis/WrappedERC20' 5 | 6 | export const wrap = async ( 7 | wrapperTokenAddress: string, 8 | underlyingTokenAddress: string, 9 | amount: BigNumberish, 10 | forAddress: string, 11 | signer: Signer, 12 | ) => { 13 | const wrapper = new Contract(wrapperTokenAddress, WRAPPED_ERC20_ABI, signer) 14 | const underlying = new Contract(underlyingTokenAddress, ERC20_ABI, signer) 15 | const sender = await signer.getAddress() 16 | const allowance = await underlying.allowance(sender, wrapperTokenAddress) 17 | if (allowance.lt(amount)) { 18 | await (await underlying.approve(wrapperTokenAddress, amount)).wait() 19 | } 20 | if (sender.toLowerCase() !== forAddress.toLowerCase()) { 21 | return wrapper.depositFor(forAddress, amount) as Promise 22 | } 23 | return wrapper.deposit(amount) as Promise 24 | } 25 | 26 | export const unwrap = async (wrapperTokenAddress: string, amount: BigNumberish, forAddress: string, signer: Signer) => { 27 | const wrapper = new Contract(wrapperTokenAddress, WRAPPED_ERC20_ABI, signer) 28 | return wrapper.burnTo(forAddress, amount) as Promise 29 | } 30 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 3 | darkMode: false, 4 | variants: { 5 | animation: ['responsive', 'motion-safe', 'motion-reduce'], 6 | extend: {}, 7 | }, 8 | theme: { 9 | extend: { 10 | spacing: { 11 | 1: '0.25rem', 12 | }, 13 | fontFamily: { 14 | sans: ['Avenir', 'sans-serif'], 15 | roboto: ['Roboto', 'sans-serif'], 16 | mono: ['SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', 'Courier', 'monospace'], 17 | helvetica: ['Helvetica', 'sans-serif'], 18 | }, 19 | fontSize: { 20 | base: ['1rem', '1.5rem'], 21 | '2xl': ['1.5rem', '2rem'], 22 | '3xl': ['1.8rem', '2.2rem'], 23 | '4xl': ['2rem', '2.5rem'], 24 | sm: ['14px', '16px'], 25 | md: ['16px', '20px'], 26 | xs: ['12px', '14px'], 27 | xxs: ['10px', '12px'], 28 | }, 29 | fontWeight: { 30 | regular: 300, 31 | bold: 700, 32 | semiBold: 500, 33 | }, 34 | colors: { 35 | primary: '#FF2D55', 36 | primaryDark: '#EE2A4F', 37 | secondary: '#912dff', 38 | secondaryDark: '#7424CC', 39 | 40 | black: '#000', 41 | white: '#FFF', 42 | 43 | green: '#7ea676', 44 | greenDark: '#268011', 45 | greenLight: '#86de26', 46 | 47 | gray: '#979797', 48 | lightGray: '#DDDDDD', 49 | mediumGray: '#4A4A4A', 50 | darkGray: '#1D1D1D', 51 | lightBlack: '#363636', 52 | 53 | link: '#0D23EE', 54 | radicalRed: '#FF2D55', 55 | paleBlue: '#F9F9F9', 56 | '0D23EE': '#0D23EE', 57 | }, 58 | borderRadius: { 59 | none: '0px', 60 | sm: '10px', 61 | }, 62 | // boxShadow: { 63 | // none: 'none', 64 | // }, 65 | 66 | // TODO:remove 67 | boxShadow: { 68 | all: '0 0px 16px rgba(0, 0, 0, 0.3)', 69 | 'all-xs': '0 0px 8px rgba(0, 0, 0, 0.3)', 70 | }, 71 | 72 | width: { 73 | '16px': '16px', 74 | '65px': '65px', 75 | '80px': '80px', 76 | '120px': '120px', 77 | '336px': '336px', 78 | btnsm: '120px', 79 | sm: '638px', 80 | md: '768px', 81 | lg: '1024px', 82 | xl: '1280px', 83 | '2xl': '1536px', 84 | }, 85 | height: { 86 | btnsm: '40px', 87 | '10px': '10px', 88 | '16px': '16px', 89 | '40px': '40px', 90 | '65px': '65px', 91 | '72px': '72px', 92 | '80px': '80px', 93 | '120px': '120px', 94 | '180px': '180px', 95 | '280px': '280px', 96 | '312px': '312px', 97 | fit: 'fit-content', 98 | }, 99 | padding: { 100 | 1: '5px', 101 | }, 102 | minHeight: { 103 | '180px': '180px', 104 | '300px': '300px', 105 | }, 106 | maxWidth: { 107 | '830px': '830px', 108 | }, 109 | }, 110 | }, 111 | variants: {}, 112 | } 113 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es6", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["src", "craco.config.js"], 21 | "exclude": ["src/sdk"] 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ampleforth/token-geyser-v2", 3 | "description": "reward distribution protocol", 4 | "version": "0.1.0", 5 | "license": "GPL-3.0", 6 | "author": "dev-support@ampleforth.org", 7 | "main": "dist/index.js", 8 | "scripts": { 9 | "compile": "yarn hardhat compile", 10 | "test": "yarn hardhat test", 11 | "profile": "REPORT_GAS=true yarn hardhat test", 12 | "coverage": "yarn hardhat coverage", 13 | "format": "yarn prettier --config .prettierrc --write '**/*.ts' 'contracts/**/*.sol'", 14 | "lint": "yarn solhint 'contracts/**/*.sol'", 15 | "build": "yarn tsc --project tsconfig.build.json" 16 | }, 17 | "pre-commit": [ 18 | "format", 19 | "lint" 20 | ], 21 | "devDependencies": { 22 | "@nomiclabs/hardhat-ethers": "^2.0.1", 23 | "@nomiclabs/hardhat-etherscan": "^2.1.8", 24 | "@nomiclabs/hardhat-waffle": "^2.0.1", 25 | "@openzeppelin/contracts": "^3.4.0-solc-0.7", 26 | "@openzeppelin/contracts-upgradeable": "^3.4.0-solc-0.7-2", 27 | "@openzeppelin/hardhat-upgrades": "^1.6.0", 28 | "@types/chai": "^4.2.14", 29 | "@types/mocha": "^8.0.3", 30 | "@types/node": "^14.14.6", 31 | "@uniswap/lib": "^4.0.1-alpha", 32 | "chai": "^4.2.0", 33 | "dotenv": "^16.3.1", 34 | "ethereum-waffle": "^3.2.1", 35 | "ethers": "5.0.31", 36 | "hardhat": "^2.0.10", 37 | "hardhat-gas-reporter": "^1.0.4", 38 | "pre-commit": "^1.2.2", 39 | "prettier": "^2.1.2", 40 | "prettier-plugin-solidity": "^1.0.0-beta.3", 41 | "solhint": "^3.3.1", 42 | "solhint-plugin-prettier": "^0.0.5", 43 | "solidity-coverage": "0.7.13", 44 | "ts-node": "^9.0.0", 45 | "typescript": "^4.0.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sdk: -------------------------------------------------------------------------------- 1 | frontend/src/sdk -------------------------------------------------------------------------------- /subgraph/.gitignore: -------------------------------------------------------------------------------- 1 | subgraph.yaml -------------------------------------------------------------------------------- /subgraph/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "bracketSpacing": true, 6 | "printWidth": 99 7 | } 8 | -------------------------------------------------------------------------------- /subgraph/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampleforth/token-geyser-v2/d8275b8e8e20656e492d32100932c33c706257b6/subgraph/.yarn/install-state.gz -------------------------------------------------------------------------------- /subgraph/README.md: -------------------------------------------------------------------------------- 1 | ## Geyser subgraph 2 | 3 | The Graph is a tool for for indexing events emitted on the Ethereum blockchain. It provides you with an easy-to-use GraphQL API. 4 | 5 | ``` 6 | Public graphql endpoint: 7 | https://api.thegraph.com/subgraphs/name/ampleforth/ampleforth-token-geyser-v2 8 | ``` 9 | 10 | ## Getting started 11 | 12 | Run a local instance of the graph node: 13 | 14 | ``` 15 | git clone https://github.com/graphprotocol/graph-node 16 | cd graph-node/docker 17 | 18 | # update docker-compose.yaml with alchemy rpc endpoint 19 | docker-compose up 20 | 21 | # NOTE: Ensure that the docker container is able to access the internet 22 | ``` 23 | 24 | Setup project: 25 | ``` 26 | yarn 27 | ``` 28 | 29 | To build and deploy the subgraph to the graph hosted service: 30 | 31 | ``` 32 | # local deployment 33 | ./scripts/deploy-local.sh mainnet ampleforth-token-geyser-v2 34 | 35 | # prod deployment 36 | ./scripts/deploy.sh mainnet ampleforth-token-geyser-v2 37 | ``` -------------------------------------------------------------------------------- /subgraph/abis/IInstanceRegistry.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-artifact-1", 3 | "contractName": "IInstanceRegistry", 4 | "sourceName": "contracts/Factory/InstanceRegistry.sol", 5 | "abi": [ 6 | { 7 | "anonymous": false, 8 | "inputs": [ 9 | { 10 | "indexed": false, 11 | "internalType": "address", 12 | "name": "instance", 13 | "type": "address" 14 | } 15 | ], 16 | "name": "InstanceAdded", 17 | "type": "event" 18 | }, 19 | { 20 | "anonymous": false, 21 | "inputs": [ 22 | { 23 | "indexed": false, 24 | "internalType": "address", 25 | "name": "instance", 26 | "type": "address" 27 | } 28 | ], 29 | "name": "InstanceRemoved", 30 | "type": "event" 31 | }, 32 | { 33 | "inputs": [ 34 | { 35 | "internalType": "uint256", 36 | "name": "index", 37 | "type": "uint256" 38 | } 39 | ], 40 | "name": "instanceAt", 41 | "outputs": [ 42 | { 43 | "internalType": "address", 44 | "name": "instance", 45 | "type": "address" 46 | } 47 | ], 48 | "stateMutability": "view", 49 | "type": "function" 50 | }, 51 | { 52 | "inputs": [], 53 | "name": "instanceCount", 54 | "outputs": [ 55 | { 56 | "internalType": "uint256", 57 | "name": "count", 58 | "type": "uint256" 59 | } 60 | ], 61 | "stateMutability": "view", 62 | "type": "function" 63 | }, 64 | { 65 | "inputs": [ 66 | { 67 | "internalType": "address", 68 | "name": "instance", 69 | "type": "address" 70 | } 71 | ], 72 | "name": "isInstance", 73 | "outputs": [ 74 | { 75 | "internalType": "bool", 76 | "name": "validity", 77 | "type": "bool" 78 | } 79 | ], 80 | "stateMutability": "view", 81 | "type": "function" 82 | } 83 | ], 84 | "bytecode": "0x", 85 | "deployedBytecode": "0x", 86 | "linkReferences": {}, 87 | "deployedLinkReferences": {} 88 | } 89 | -------------------------------------------------------------------------------- /subgraph/abis/IPowerSwitch.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format": "hh-sol-artifact-1", 3 | "contractName": "IPowerSwitch", 4 | "sourceName": "contracts/PowerSwitch/PowerSwitch.sol", 5 | "abi": [ 6 | { 7 | "anonymous": false, 8 | "inputs": [], 9 | "name": "EmergencyShutdown", 10 | "type": "event" 11 | }, 12 | { 13 | "anonymous": false, 14 | "inputs": [], 15 | "name": "PowerOff", 16 | "type": "event" 17 | }, 18 | { 19 | "anonymous": false, 20 | "inputs": [], 21 | "name": "PowerOn", 22 | "type": "event" 23 | }, 24 | { 25 | "inputs": [], 26 | "name": "emergencyShutdown", 27 | "outputs": [], 28 | "stateMutability": "nonpayable", 29 | "type": "function" 30 | }, 31 | { 32 | "inputs": [], 33 | "name": "getPowerController", 34 | "outputs": [ 35 | { 36 | "internalType": "address", 37 | "name": "controller", 38 | "type": "address" 39 | } 40 | ], 41 | "stateMutability": "view", 42 | "type": "function" 43 | }, 44 | { 45 | "inputs": [], 46 | "name": "getStatus", 47 | "outputs": [ 48 | { 49 | "internalType": "enum IPowerSwitch.State", 50 | "name": "status", 51 | "type": "uint8" 52 | } 53 | ], 54 | "stateMutability": "view", 55 | "type": "function" 56 | }, 57 | { 58 | "inputs": [], 59 | "name": "isOffline", 60 | "outputs": [ 61 | { 62 | "internalType": "bool", 63 | "name": "status", 64 | "type": "bool" 65 | } 66 | ], 67 | "stateMutability": "view", 68 | "type": "function" 69 | }, 70 | { 71 | "inputs": [], 72 | "name": "isOnline", 73 | "outputs": [ 74 | { 75 | "internalType": "bool", 76 | "name": "status", 77 | "type": "bool" 78 | } 79 | ], 80 | "stateMutability": "view", 81 | "type": "function" 82 | }, 83 | { 84 | "inputs": [], 85 | "name": "isShutdown", 86 | "outputs": [ 87 | { 88 | "internalType": "bool", 89 | "name": "status", 90 | "type": "bool" 91 | } 92 | ], 93 | "stateMutability": "view", 94 | "type": "function" 95 | }, 96 | { 97 | "inputs": [], 98 | "name": "powerOff", 99 | "outputs": [], 100 | "stateMutability": "nonpayable", 101 | "type": "function" 102 | }, 103 | { 104 | "inputs": [], 105 | "name": "powerOn", 106 | "outputs": [], 107 | "stateMutability": "nonpayable", 108 | "type": "function" 109 | } 110 | ], 111 | "bytecode": "0x", 112 | "deployedBytecode": "0x", 113 | "linkReferences": {}, 114 | "deployedLinkReferences": {} 115 | } 116 | -------------------------------------------------------------------------------- /subgraph/configs/avalanche.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "avalanche", 3 | "startBlock": 8279332, 4 | "geyserRegistry": "0x60156bB86e9125639c624712a360FD3AbBb52421", 5 | "vaultFactory": "0xceD5A1061F5507172059FE760CA2e9F050caBF02", 6 | "crucibleFactory": "0x0000000000000000000000000000000000000000" 7 | } -------------------------------------------------------------------------------- /subgraph/configs/kovan.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "kovan", 3 | "startBlock": 28664756, 4 | "geyserRegistry": "0x87D44039dC98D9e44bD3EB49a62E8289b21895E2", 5 | "vaultFactory": "0x3fBbe79B510478CaFC58c7d321188AA1f0a7F88c", 6 | "crucibleFactory": "0x0000000000000000000000000000000000000000" 7 | } -------------------------------------------------------------------------------- /subgraph/configs/mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "mainnet", 3 | "startBlock": 12797939, 4 | "geyserRegistry": "0xFc43803F203e3821213bE687120aD44C8a21A7e7", 5 | "vaultFactory": "0x8A09fFA4d4310c7F59DC538a1481D8Ba2214Cef0", 6 | "crucibleFactory": "0x54e0395CFB4f39beF66DBCd5bD93Cca4E9273D56" 7 | } -------------------------------------------------------------------------------- /subgraph/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ampleforthorg/token-gesyser-v2", 3 | "version": "1.0.0", 4 | "license": "GPL-3.0-or-later", 5 | "scripts": { 6 | "auth": "graph auth --studio", 7 | "codegen": "graph codegen --output-dir ./generated", 8 | "build": "graph build", 9 | "lint": "yarn prettier --config .prettierrc --write '**/*.ts'", 10 | "create-local": "graph create --node http://localhost:8020/ ampleforth/ampleforth-token-geyser-v2", 11 | "remove-local": "graph remove --node http://localhost:8020/ ampleforth/ampleforth-token-geyser-v2", 12 | "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 ampleforth/ampleforth-token-geyser-v2", 13 | "test": "echo 'TO_BE_IMPL'" 14 | }, 15 | "devDependencies": { 16 | "@graphprotocol/graph-cli": "^0.21.1", 17 | "@graphprotocol/graph-ts": "^0.20.0", 18 | "@typescript-eslint/eslint-plugin": "^2.0.0", 19 | "@typescript-eslint/parser": "^2.0.0", 20 | "eslint": "^6.2.2", 21 | "eslint-config-prettier": "^6.1.0", 22 | "mustache": "^4.2.0", 23 | "prettier": "^1.18.2", 24 | "typescript": "^3.5.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /subgraph/schema.graphql: -------------------------------------------------------------------------------- 1 | enum GeyserStatus { 2 | Online 3 | Offline 4 | Shutdown 5 | } 6 | 7 | type PowerSwitch @entity { 8 | id: ID! # ${powerSwitch.address} 9 | status: GeyserStatus! 10 | } 11 | 12 | type Geyser @entity { 13 | id: ID! # ${geyser.address} 14 | powerSwitch: PowerSwitch! 15 | rewardPool: Bytes! 16 | rewardPoolBalances: [RewardPoolBalance]! @derivedFrom(field: "geyser") 17 | unlockedReward: BigInt! 18 | rewardBalance: BigInt! 19 | stakingToken: Bytes! 20 | rewardToken: Bytes! 21 | scalingFloor: BigInt! 22 | scalingCeiling: BigInt! 23 | scalingTime: BigInt! 24 | totalStake: BigInt! 25 | totalStakeUnits: BigInt! 26 | bonusTokens: [Bytes!]! 27 | lastUpdate: BigInt! 28 | rewardSchedules: [RewardSchedule]! @derivedFrom(field: "geyser") 29 | locks: [Lock]! @derivedFrom(field: "geyser") 30 | dailyStats: [GeyserDailyStat!]! @derivedFrom(field: "geyser") 31 | } 32 | 33 | type User @entity { 34 | id: ID! # ${account.address} 35 | vaults: [Vault]! @derivedFrom(field: "owner") 36 | } 37 | 38 | type Vault @entity { 39 | id: ID! # ${vault.address} 40 | owner: User! 41 | nonce: BigInt! 42 | claimedReward: [ClaimedReward]! @derivedFrom(field: "vault") 43 | locks: [Lock]! @derivedFrom(field: "vault") 44 | } 45 | 46 | type Lock @entity { 47 | id: ID! # ${vault.id}-${geyser.id}-${token.address} 48 | vault: Vault! 49 | geyser: Geyser! 50 | token: Bytes! 51 | amount: BigInt! 52 | stakeUnits: BigInt 53 | lastUpdate: BigInt! 54 | } 55 | 56 | type LockedBalance @entity { 57 | id: ID! # ${vault.id}-${token.address} 58 | vault: Vault! 59 | token: Bytes! 60 | amount: BigInt! 61 | lastUpdate: BigInt! 62 | } 63 | 64 | type RewardSchedule @entity { 65 | id: ID! # ${geyser.id}-${index} 66 | geyser: Geyser! 67 | rewardAmount: BigInt! 68 | duration: BigInt! 69 | start: BigInt! 70 | shares: BigInt! 71 | } 72 | 73 | type ClaimedReward @entity { 74 | id: ID! # ${vault.id-token.address} 75 | vault: Vault! 76 | token: Bytes! 77 | amount: BigInt! 78 | lastUpdate: BigInt! 79 | } 80 | 81 | type RewardPoolBalance @entity { 82 | id: ID! # ${rewardPool.address}-${token.address} 83 | geyser: Geyser! 84 | pool: Bytes! 85 | token: Bytes! 86 | balance: BigInt! 87 | lastUpdate: BigInt! 88 | } 89 | 90 | type GeyserDailyStat @entity { 91 | "-" 92 | id: ID! 93 | geyser: Geyser! 94 | timestamp: BigInt! 95 | unlockedReward: BigInt! 96 | rewardBalance: BigInt! 97 | totalStake: BigInt! 98 | totalStakeUnits: BigInt! 99 | rewardPoolBalances: [BigInt!]! 100 | } -------------------------------------------------------------------------------- /subgraph/scripts/deploy-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | yarn mustache ./configs/$1.json subgraph.template.yaml > ./subgraph.yaml 5 | 6 | yarn codegen 7 | 8 | yarn build 9 | 10 | yarn create-local 11 | 12 | yarn deploy-local 13 | -------------------------------------------------------------------------------- /subgraph/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | yarn mustache ./configs/$1.json subgraph.template.yaml > ./subgraph.yaml 5 | 6 | yarn codegen 7 | 8 | yarn build 9 | 10 | yarn graph deploy $2 \ 11 | --node https://subgraphs.alchemy.com/api/subgraphs/deploy \ 12 | --deploy-key $GRAPH_AUTH -------------------------------------------------------------------------------- /subgraph/src/rebasingToken.ts: -------------------------------------------------------------------------------- 1 | import { log, dataSource, Address, BigInt } from '@graphprotocol/graph-ts' 2 | import { LogRebase, Rebase } from '../generated/templates/RebasingERC20/RebasingERC20' 3 | import { stringToAddress, dayTimestamp } from './utils' 4 | import { updateGeyser } from './geyser' 5 | 6 | function _handleRebase(address: Address, timestamp: BigInt): void { 7 | let context = dataSource.context() 8 | if (context.get('geyser') != null) { 9 | let id = context.getString('geyser') 10 | log.warning('geyserRefresh: {}', [id]) 11 | updateGeyser(stringToAddress(id), dayTimestamp(timestamp)) 12 | } 13 | } 14 | 15 | export function handleRebase(event: Rebase): void { 16 | log.warning('triggered handleRebase', []) 17 | _handleRebase(event.address, event.block.timestamp) 18 | } 19 | 20 | export function handleLogRebase(event: LogRebase): void { 21 | log.warning('triggered handleLogRebase', []) 22 | _handleRebase(event.address, event.block.timestamp) 23 | } 24 | -------------------------------------------------------------------------------- /subgraph/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Address, BigInt } from '@graphprotocol/graph-ts' 2 | 3 | export let bigIntZero = BigInt.fromI32(0) 4 | 5 | export const dayTimestamp = (timestamp: BigInt): BigInt => { 6 | return timestamp.minus(timestamp % BigInt.fromI32(24 * 3600)) 7 | } 8 | 9 | export const stringToAddress = (id: string): Address => { 10 | return Address.fromString(id) 11 | } 12 | -------------------------------------------------------------------------------- /subgraph/src/vault.ts: -------------------------------------------------------------------------------- 1 | // assembly script imports 2 | import { Address, BigInt } from '@graphprotocol/graph-ts' 3 | 4 | // template creation imports 5 | import { InstanceAdded } from '../generated/VaultFactory/InstanceRegistry' 6 | import { VaultTemplate } from '../generated/templates' 7 | 8 | // handler imports 9 | import { Lock, LockedBalance, User, Vault } from '../generated/schema' 10 | import { Transfer } from '../generated/UniversalVaultNFT/ERC721' 11 | 12 | // entity imports 13 | import { Locked, RageQuit, Unlocked, VaultContract } from '../generated/templates/VaultTemplate/VaultContract' 14 | 15 | // template instantiation 16 | function updateVault(vaultAddress: Address): void { 17 | let vault = new Vault(vaultAddress.toHex()) 18 | 19 | let vaultContract = VaultContract.bind(vaultAddress) 20 | 21 | let owner = vaultContract.owner() 22 | let user = new User(owner.toHex()) 23 | 24 | vault.owner = owner.toHex() 25 | vault.nonce = vaultContract.getNonce() 26 | 27 | user.save() 28 | vault.save() 29 | } 30 | 31 | export function handleNewVault(event: InstanceAdded): void { 32 | VaultTemplate.create(event.params.instance) 33 | 34 | updateVault(event.params.instance) 35 | } 36 | 37 | // event handlers 38 | function updateLock(vaultAddress: Address, geyserAddress: Address, tokenAddress: Address, timestamp: BigInt): void { 39 | updateVault(vaultAddress) 40 | 41 | let vaultContract = VaultContract.bind(vaultAddress) 42 | let lock = new Lock(vaultAddress.toHex() + '-' + geyserAddress.toHex() + '-' + tokenAddress.toHex()) 43 | let lockedBalance = new LockedBalance(vaultAddress.toHex() + '-' + tokenAddress.toHex()) 44 | 45 | lock.amount = vaultContract.getBalanceDelegated(tokenAddress, geyserAddress) 46 | lock.geyser = geyserAddress.toHex() 47 | lock.vault = vaultAddress.toHex() 48 | lock.token = tokenAddress 49 | lock.lastUpdate = timestamp 50 | 51 | lockedBalance.amount = vaultContract.getBalanceLocked(tokenAddress) 52 | lockedBalance.vault = vaultAddress.toHex() 53 | lockedBalance.token = tokenAddress 54 | lockedBalance.lastUpdate = timestamp 55 | 56 | lock.save() 57 | lockedBalance.save() 58 | } 59 | 60 | export function handleLocked(event: Locked): void { 61 | updateLock(event.address, event.params.delegate, event.params.token, event.block.timestamp) 62 | } 63 | 64 | export function handleUnlocked(event: Unlocked): void { 65 | updateLock(event.address, event.params.delegate, event.params.token, event.block.timestamp) 66 | } 67 | 68 | export function handleRageQuit(event: RageQuit): void { 69 | updateLock(event.address, event.params.delegate, event.params.token, event.block.timestamp) 70 | } 71 | 72 | function bigIntToAddress(value: BigInt): Address { 73 | return Address.fromString(value.toHex().slice(2).padStart(40, '0')) 74 | } 75 | 76 | export function handleTransfer(event: Transfer): void { 77 | let from = new User(event.params.from.toHex()) 78 | updateVault(bigIntToAddress(event.params.tokenId)) 79 | from.save() 80 | } 81 | -------------------------------------------------------------------------------- /test/Access/ERC1271.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 2 | import { expect } from 'chai' 3 | import { Contract } from 'ethers' 4 | import { hashMessage, keccak256 } from 'ethers/lib/utils' 5 | import { ethers } from 'hardhat' 6 | import { deployContract } from '../utils' 7 | 8 | describe('ERC1271', function () { 9 | let accounts: SignerWithAddress[] 10 | let MockERC1271: Contract 11 | const message = hashMessage('ERC1271 test message') 12 | let VALID_SIG: string 13 | const INVALID_SIG = '0x00000000' 14 | 15 | function toEthSignedMessageHash(messageHex: string) { 16 | const messageBuffer = Buffer.from(messageHex.substring(2), 'hex') 17 | const prefix = Buffer.from(`\u0019Ethereum Signed Message:\n${messageBuffer.length}`) 18 | return keccak256(Buffer.concat([prefix, messageBuffer])) 19 | } 20 | 21 | beforeEach(async function () { 22 | // prepare signers 23 | accounts = await ethers.getSigners() 24 | // deploy mock 25 | MockERC1271 = await deployContract('MockERC1271', [accounts[0].address]) 26 | VALID_SIG = MockERC1271.interface.getSighash('isValidSignature(bytes32,bytes)') 27 | }) 28 | 29 | describe('isValidSignature', function () { 30 | it('should return error value if signed by account other than owner', async function () { 31 | const sig = await accounts[1].signMessage(message) 32 | expect(await MockERC1271.isValidSignature(toEthSignedMessageHash(message), sig)).to.eq(INVALID_SIG) 33 | }) 34 | 35 | it('should revert if signature has incorrect length', async function () { 36 | const sig = await accounts[0].signMessage(message) 37 | expect(MockERC1271.isValidSignature(toEthSignedMessageHash(message), sig.slice(0, 10))).to.be.revertedWith( 38 | 'ECDSA: invalid signature length', 39 | ) 40 | }) 41 | 42 | it('should return success value if signed by owner', async function () { 43 | const sig = await accounts[0].signMessage(message) 44 | expect(await MockERC1271.isValidSignature(hashMessage(message), sig)).to.eq(VALID_SIG) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/VaultFactory.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' 2 | import { expect } from 'chai' 3 | import { Contract } from 'ethers' 4 | import { ethers } from 'hardhat' 5 | import { create2Instance, createInstance, deployContract } from './utils' 6 | 7 | describe('VaultFactory', function () { 8 | let accounts: SignerWithAddress[] 9 | let factory: Contract, template: Contract 10 | 11 | beforeEach(async function () { 12 | // prepare signers 13 | accounts = await ethers.getSigners() 14 | // deploy template 15 | template = await deployContract('UniversalVault') 16 | // deploy factory 17 | factory = await deployContract('VaultFactory', [template.address]) 18 | }) 19 | 20 | describe('getTemplate', function () { 21 | it('should succeed', async function () { 22 | expect(await factory.getTemplate()).to.be.eq(template.address) 23 | }) 24 | }) 25 | describe('create', function () { 26 | it('should succeed', async function () { 27 | await createInstance('UniversalVault', factory, accounts[0]) 28 | }) 29 | it('should successfully call owner', async function () { 30 | const vault = await createInstance('UniversalVault', factory, accounts[0]) 31 | expect(await vault.owner()).to.eq(accounts[0].address) 32 | }) 33 | }) 34 | describe('create2', function () { 35 | it('should succeed', async function () { 36 | await create2Instance('UniversalVault', factory, accounts[0], ethers.utils.randomBytes(32)) 37 | }) 38 | it('should successfully call owner', async function () { 39 | const vault = await create2Instance('UniversalVault', factory, accounts[0], ethers.utils.randomBytes(32)) 40 | expect(await vault.owner()).to.eq(accounts[0].address) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "downlevelIteration": true, 9 | "outDir": "dist" 10 | }, 11 | "include": ["sdk"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "downlevelIteration": true, 9 | "outDir": "dist" 10 | }, 11 | "include": ["test"], 12 | "files": ["hardhat.config.ts"] 13 | } 14 | --------------------------------------------------------------------------------