├── .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 |
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/checkmark_light.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/clipboard.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/rewardSymbol.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
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 |
29 | )
30 | }
31 |
32 | const SelectContainer = styled.div`
33 | ${tw`w-6/12 pt-2 pr-2`}
34 | `
35 |
--------------------------------------------------------------------------------
/frontend/src/components/HeaderWalletButton.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react'
2 | import styled from 'styled-components/macro'
3 | import tw from 'twin.macro'
4 | import Web3Context from 'context/Web3Context'
5 | import { AppContext } from 'context/AppContext'
6 | import { displayAddr } from 'utils/formatDisplayAddress'
7 | import { NamedColors } from 'styling/colors'
8 | import { Mode } from '../constants'
9 |
10 | export const HeaderWalletButton = () => {
11 | const { connectWallet, disconnectWallet, wallet, address } = useContext(Web3Context)
12 | const { mode, toggleMode } = useContext(AppContext)
13 |
14 | const handleButtonClick = async () => {
15 | if (wallet) {
16 | await disconnectWallet(wallet)
17 | if (mode !== Mode.GEYSERS) {
18 | toggleMode()
19 | }
20 | } else {
21 | connectWallet()
22 | }
23 | }
24 |
25 | return (
26 |
27 |
32 |
33 | )
34 | }
35 |
36 | const ButtonContainer = styled.div`
37 | ${tw`flex mt-1 mr-3`}
38 | `
39 |
40 | const Button = styled.button<{ connected: boolean }>`
41 | ${tw`w-full border-lightGray rounded w-120px h-40px text-white font-bold text-sm transition-all duration-300 ease-in-out`}
42 | ${({ connected }) => (connected ? tw`bg-secondary` : tw`bg-primary hover:bg-primaryDark`)}
43 | `
44 |
--------------------------------------------------------------------------------
/frontend/src/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { Dialog, Transition } from '@headlessui/react'
2 | import { createContext, Fragment, MutableRefObject, useRef } from 'react'
3 | import styled from 'styled-components/macro'
4 | import tw from 'twin.macro'
5 |
6 | // Modal needs a focusable element to function correctly.
7 | // Use a context to pass the ref object down to Modal.Body,
8 | // which will make the modal focus on Modal.Body, if given
9 | const ModalContext = createContext<{
10 | ref: MutableRefObject | null
11 | }>({
12 | ref: null,
13 | })
14 |
15 | const Title: React.FC = ({ children }) => (
16 |
17 | {children}
18 |
19 | )
20 |
21 | const Body: React.FC = ({ children }) => (
22 |
23 | {({ ref }) => (
24 |
25 | {children}
26 |
27 | )}
28 |
29 | )
30 |
31 | const Footer: React.FC = ({ children }) => {children}
32 |
33 | interface ModalSubComponents {
34 | Title: React.FC
35 | Body: React.FC
36 | Footer: React.FC
37 | }
38 |
39 | interface Props {
40 | onClose: () => void
41 | open: boolean
42 | initialFocus?: MutableRefObject
43 | disableClose?: boolean
44 | }
45 |
46 | const ModalRoot: React.FC = ({ open, onClose, disableClose, children, initialFocus }) => {
47 | const ref = initialFocus ?? useRef(null)
48 |
49 | return (
50 |
51 |
52 |
77 |
78 |
79 | )
80 | }
81 |
82 | export const Modal: React.FC & ModalSubComponents = Object.assign(ModalRoot, { Title, Body, Footer })
83 |
84 | const Container = styled.div`
85 | ${tw`min-h-screen px-4 text-center`}
86 | `
87 |
88 | const ContentContainer = styled.div`
89 | ${tw`inline-block min-h-180px w-full w-sm p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl`}
90 | `
91 |
92 | const MessageContainer = styled.div`
93 | ${tw`mt-2`}
94 | `
95 |
96 | const Message = styled.div`
97 | ${tw`text-sm leading-5`}
98 | `
99 |
100 | const FooterContainer = styled.div`
101 | ${tw`mt-8 flex justify-center`}
102 | `
103 |
--------------------------------------------------------------------------------
/frontend/src/components/PageLoader.css:
--------------------------------------------------------------------------------
1 | .GenericProgress-Container {
2 | position: fixed; /* Cover the entire viewport */
3 | top: 0;
4 | left: 0;
5 | width: 100%;
6 | height: 100%;
7 | background: rgba(255, 255, 255, 0.8); /* Transparent white background */
8 | color: #000;
9 | display: flex; /* Flexbox for loader placement */
10 | align-items: flex-start; /* Align loader to the top */
11 | justify-content: center; /* Center loader horizontally */
12 | padding-top: 250px; /* Add some space from the top */
13 | z-index: 9999; /* Ensure it is above all other elements */
14 | }
15 |
16 | .page-loader {
17 | display: inline-grid;
18 | }
19 | .page-loader:before,
20 | .page-loader:after {
21 | content: '';
22 | grid-area: 1/1;
23 | height: 30px;
24 | aspect-ratio: 6;
25 | --c: #0000 64%, #000 66% 98%, #0000 101%;
26 | background: radial-gradient(35% 146% at 50% 159%, var(--c)) 0 0,
27 | radial-gradient(35% 146% at 50% -59%, var(--c)) 25% 100%;
28 | background-size: calc(100% / 3) 50%;
29 | background-repeat: repeat-x;
30 | clip-path: inset(0 100% 0 0);
31 | animation: l10 1.5s infinite linear;
32 | }
33 | .page-loader:after {
34 | scale: -1;
35 | }
36 |
37 | @keyframes l10 {
38 | 50% {
39 | clip-path: inset(0);
40 | }
41 | to {
42 | clip-path: inset(0 0 0 100%);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/frontend/src/components/PageLoader.tsx:
--------------------------------------------------------------------------------
1 | // react
2 | import React from 'react'
3 | // styles
4 | import './PageLoader.css'
5 |
6 | export const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000))
7 |
8 | function PageLoader() {
9 | return (
10 | <>
11 |
14 | >
15 | )
16 | }
17 |
18 | export default PageLoader
19 |
--------------------------------------------------------------------------------
/frontend/src/components/PositiveInput.tsx:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'ethers'
2 | import { formatUnits, parseUnits } from 'ethers/lib/utils'
3 | import styled from 'styled-components/macro'
4 | import tw from 'twin.macro'
5 |
6 | interface Props extends Omit, 'onChange'> {
7 | precision: number
8 | maxValue: BigNumber
9 | onChange?: (value: string) => void
10 | skipMaxEnforcement?: boolean
11 | }
12 |
13 | export const PositiveInput: React.FC = (props) => {
14 | const { onChange, precision, maxValue, skipMaxEnforcement } = props
15 |
16 | const respectsPrecision = (value: string) => {
17 | if (value) {
18 | const parts = value.split('.')
19 | return parts.length > 1 ? parts[1].length <= precision : true
20 | }
21 | return true
22 | }
23 |
24 | const respectsMax = (value: string) => {
25 | if (skipMaxEnforcement) {
26 | return true
27 | }
28 | if (value) {
29 | return parseUnits(value, precision).lte(maxValue)
30 | }
31 | return true
32 | }
33 |
34 | const positiveOnChange = (e: React.ChangeEvent) => {
35 | const pattern = new RegExp(`(^\\d+$|^\\d+\\.\\d+$|^\\d+\\.$|^$)`)
36 | const { value } = e.currentTarget
37 | if (onChange && pattern.test(value) && respectsPrecision(value) && respectsMax(value)) {
38 | onChange(value)
39 | }
40 | }
41 |
42 | const setMax = () => {
43 | if (onChange) onChange(formatUnits(maxValue, precision))
44 | }
45 |
46 | return (
47 |
48 |
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 |
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 |
})
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 | Manage balances
40 |
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 |
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 Liquidity
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 |
--------------------------------------------------------------------------------