├── .env.example
├── .gas-snapshot
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .gitmodules
├── LICENSE
├── README.md
├── defender.config.json
├── docs
└── docs.md
├── foundry.toml
├── remappings.txt
├── script
├── USDC.s.sol
├── Upgrade.s.sol
├── deploy
│ ├── DeployBase.s.sol
│ ├── DeployMainnet.s.sol
│ └── DeploySepolia.s.sol
└── pools
│ ├── AddBase.s.sol
│ └── vrtx
│ ├── AddMainnet.s.sol
│ └── AddSepolia.s.sol
├── slither.config.json
├── src
├── Distributor.sol
├── VertexManager.sol
├── VertexProcessor.sol
├── VertexRouter.sol
├── VertexStorage.sol
└── interfaces
│ ├── IClearinghouse.sol
│ ├── IEndpoint.sol
│ └── IVertexManager.sol
└── test
├── Distributor.t.sol
├── VertexManager.t.sol
├── VertexManagerUpgrade.t.sol
├── invariants
├── VertexManager.invariants.t.sol
└── VertexManagerHandler.sol
└── utils
├── AddressSet.sol
├── MockToken.sol
├── MockTokenDecimals.sol
├── ProcessQueue.sol
└── Utils.sol
/.env.example:
--------------------------------------------------------------------------------
1 | ARBITRUM_RPC_URL = 'https://arb1.croswap.com/rpc'
2 | SEPOLIA_RPC_URL = 'https://arbitrum-sepolia.blockpi.network/v1/rpc/public'
--------------------------------------------------------------------------------
/.gas-snapshot:
--------------------------------------------------------------------------------
1 | DeployGoerli:test() (gas: 120)
2 | MockToken:test() (gas: 208)
3 | TestVertexManager:testAddAndUpdatePool() (gas: 869592)
4 | TestVertexManager:testAddInvalidPoolToken() (gas: 1626435)
5 | TestVertexManager:testAddPoolTokens() (gas: 1124291)
6 | TestVertexManager:testClaimChecks(uint72) (runs: 100, μ: 2231131, ~: 2231927)
7 | TestVertexManager:testClaimsPaused() (gas: 27408)
8 | TestVertexManager:testDepositWithNoApproval(uint240) (runs: 100, μ: 1390253, ~: 1390112)
9 | TestVertexManager:testDepositWithNoBalance(uint240) (runs: 100, μ: 1163155, ~: 1162991)
10 | TestVertexManager:testDepositWithNotEnoughApproval(uint240) (runs: 100, μ: 1442132, ~: 1443367)
11 | TestVertexManager:testDepositsPaused() (gas: 29893)
12 | TestVertexManager:testFailDoubleInitiliaze() (gas: 13187)
13 | TestVertexManager:testFailGetWithdrawFee() (gas: 27897)
14 | TestVertexManager:testFailUnauthorizedUpgrade() (gas: 13164)
15 | TestVertexManager:testFailUpdateToken() (gas: 15111)
16 | TestVertexManager:testGetWithdraw() (gas: 11029)
17 | TestVertexManager:testHardcapReached() (gas: 2149855)
18 | TestVertexManager:testInitialize() (gas: 345913)
19 | TestVertexManager:testInsufficientFee() (gas: 1844925)
20 | TestVertexManager:testInvalidHardcaps() (gas: 20993)
21 | TestVertexManager:testIsPoolAdded() (gas: 22077)
22 | TestVertexManager:testPerpChecks(uint72,uint80,uint256) (runs: 100, μ: 2443318, ~: 2457542)
23 | TestVertexManager:testPerpDouble(uint144,uint160,uint256) (runs: 100, μ: 6020411, ~: 6166428)
24 | TestVertexManager:testPerpOtherReceiver(uint72,uint80,uint256) (runs: 100, μ: 4371524, ~: 4394193)
25 | TestVertexManager:testPerpSingle(uint72,uint80,uint256) (runs: 100, μ: 4327562, ~: 4342421)
26 | TestVertexManager:testPoolAdd() (gas: 1989177)
27 | TestVertexManager:testPoolChecks() (gas: 1796978)
28 | TestVertexManager:testPrice() (gas: 1077041)
29 | TestVertexManager:testSpotChecks(uint72) (runs: 100, μ: 2003805, ~: 2004004)
30 | TestVertexManager:testSpotDouble(uint144) (runs: 100, μ: 4304140, ~: 4304780)
31 | TestVertexManager:testSpotOtherReceiver(uint72) (runs: 100, μ: 3186418, ~: 3186280)
32 | TestVertexManager:testSpotSingle(uint72) (runs: 100, μ: 3158328, ~: 3158507)
33 | TestVertexManager:testTokenDecimals(uint8,uint8,uint80) (runs: 100, μ: 2084225, ~: 2084225)
34 | TestVertexManager:testUnauthorizedAddAndUpdate() (gas: 25952)
35 | TestVertexManager:testUnsupportedToken(uint72) (runs: 100, μ: 2003223, ~: 2003223)
36 | TestVertexManager:testUpdateSlowModeFee() (gas: 32210)
37 | TestVertexManager:testUpdateSlowModeFeeTooHigh() (gas: 18652)
38 | TestVertexManager:testUpdateToken() (gas: 96504)
39 | TestVertexManager:testUpgradeProxy() (gas: 7598057)
40 | TestVertexManager:testVertexBalance(uint144) (runs: 100, μ: 3118794, ~: 3118794)
41 | TestVertexManager:testWithdrawUSDCFee() (gas: 2919417)
42 | TestVertexManager:testWithdrawWithNoBalance(uint240) (runs: 100, μ: 1144780, ~: 1144701)
43 | TestVertexManager:testWithdrawWithNotEnoughBalance(uint248) (runs: 100, μ: 1884786, ~: 1900112)
44 | TestVertexManager:testWithdrawalsPaused() (gas: 27406)
45 | TestVertexManagerInvariants:invariant_conservationOfTokens() (runs: 5, calls: 50, reverts: 0)
46 | TestVertexManagerInvariants:invariant_depositorBalances() (runs: 5, calls: 50, reverts: 0)
47 | TestVertexManagerInvariants:invariant_depositsAndWithdraws() (runs: 5, calls: 50, reverts: 0)
48 | TestVertexManagerInvariants:invariant_router() (runs: 5, calls: 50, reverts: 0)
49 | TestVertexManagerInvariants:invariant_solvencyBalances() (runs: 5, calls: 50, reverts: 0)
50 | TestVertexManagerInvariants:invariant_solvencyDeposits() (runs: 5, calls: 50, reverts: 0)
51 | TestVertexManagerInvariants:invariant_withdrawBalances() (runs: 5, calls: 50, reverts: 0)
52 | TestVertexManagerInvariants:test() (gas: 230)
53 | UpgradeContract:test() (gas: 120)
54 | Utils:test() (gas: 186)
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: push
3 | env:
4 | FOUNDRY_PROFILE: ci
5 | ARBITRUM_RPC_URL: ${{ secrets.RPC }}
6 | SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }}
7 |
8 | jobs:
9 | tests:
10 | name: Foundry tests
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 | with:
16 | submodules: true
17 |
18 | - name: Install Foundry
19 | uses: foundry-rs/foundry-toolchain@v1
20 | with:
21 | version: nightly
22 |
23 | - name: Install dependencies
24 | run: forge install
25 |
26 | - name: Check contract sizes
27 | run: forge build --sizes --skip test
28 | id: build
29 |
30 | - name: Run tests
31 | run: FOUNDRY_PROFILE="deep" forge test -v
32 | id: test
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiler files
2 | cache/
3 | out/
4 |
5 | # Ignores development broadcast logs
6 | !/broadcast
7 | /broadcast/*/31337/
8 | /broadcast/**/dry-run/
9 | /broadcast
10 |
11 | # Dotenv file
12 | .env
13 |
14 | # General
15 | .DS_Store
16 | .AppleDouble
17 | .LSOverride
18 | lcov.info
19 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/forge-std"]
2 | path = lib/forge-std
3 | url = https://github.com/foundry-rs/forge-std
4 | branch = v1.5.6
5 | [submodule "lib/openzeppelin-contracts-upgradeable"]
6 | path = lib/openzeppelin-contracts-upgradeable
7 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
8 | [submodule "lib/openzeppelin-contracts"]
9 | path = lib/openzeppelin-contracts
10 | url = https://github.com/OpenZeppelin/openzeppelin-contracts
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Business Source License 1.1
2 |
3 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
4 | "Business Source License" is a trademark of MariaDB Corporation Ab.
5 |
6 | -----------------------------------------------------------------------------
7 |
8 | Parameters
9 |
10 | Licensor: Elixir Labs Ltd
11 |
12 | Licensed Work: Vertex Contracts
13 | The Licensed Work is (c) 2023 Elixir Labs Ltd
14 |
15 | Additional Use Grant: None
16 |
17 | Change Date: 2027-10-30
18 |
19 | Change License: GNU General Public License v2.0 or later
20 |
21 | -----------------------------------------------------------------------------
22 |
23 | Terms
24 |
25 | The Licensor hereby grants you the right to copy, modify, create derivative
26 | works, redistribute, and make non-production use of the Licensed Work. The
27 | Licensor may make an Additional Use Grant, above, permitting limited
28 | production use.
29 |
30 | Effective on the Change Date, or the fourth anniversary of the first publicly
31 | available distribution of a specific version of the Licensed Work under this
32 | License, whichever comes first, the Licensor hereby grants you rights under
33 | the terms of the Change License, and the rights granted in the paragraph
34 | above terminate.
35 |
36 | If your use of the Licensed Work does not comply with the requirements
37 | currently in effect as described in this License, you must purchase a
38 | commercial license from the Licensor, its affiliated entities, or authorized
39 | resellers, or you must refrain from using the Licensed Work.
40 |
41 | All copies of the original and modified Licensed Work, and derivative works
42 | of the Licensed Work, are subject to this License. This License applies
43 | separately for each version of the Licensed Work and the Change Date may vary
44 | for each version of the Licensed Work released by Licensor.
45 |
46 | You must conspicuously display this License on each original or modified copy
47 | of the Licensed Work. If you receive the Licensed Work in original or
48 | modified form from a third party, the terms and conditions set forth in this
49 | License apply to your use of that work.
50 |
51 | Any use of the Licensed Work in violation of this License will automatically
52 | terminate your rights under this License for the current and all other
53 | versions of the Licensed Work.
54 |
55 | This License does not grant you any right in any trademark or logo of
56 | Licensor or its affiliates (provided that you may use a trademark or logo of
57 | Licensor as expressly required by this License).
58 |
59 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
60 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
61 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
62 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
63 | TITLE.
64 |
65 | MariaDB hereby grants you permission to use this License’s text to license
66 | your works, and to refer to it using the trademark "Business Source License",
67 | as long as you comply with the Covenants of Licensor below.
68 |
69 | -----------------------------------------------------------------------------
70 |
71 | Covenants of Licensor
72 |
73 | In consideration of the right to use this License’s text and the "Business
74 | Source License" name and trademark, Licensor covenants to MariaDB, and to all
75 | other recipients of the licensed work to be provided by Licensor:
76 |
77 | 1. To specify as the Change License the GPL Version 2.0 or any later version,
78 | or a license that is compatible with GPL Version 2.0 or a later version,
79 | where "compatible" means that software provided under the Change License can
80 | be included in a program with software provided under GPL Version 2.0 or a
81 | later version. Licensor may specify additional Change Licenses without
82 | limitation.
83 |
84 | 2. To either: (a) specify an additional grant of rights to use that does not
85 | impose any additional restriction on the right granted in this License, as
86 | the Additional Use Grant; or (b) insert the text "None".
87 |
88 | 3. To specify a Change Date.
89 |
90 | 4. Not to modify this License in any other way.
91 |
92 | -----------------------------------------------------------------------------
93 |
94 | Notice
95 |
96 | The Business Source License (this document, or the "License") is not an Open
97 | Source license. However, the Licensed Work will eventually be made available
98 | under an Open Source License, as stated in this License.
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Elixir <> Vertex Contracts • [](https://github.com/ElixirProtocol/elixir-contracts/actions/workflows/test.yml)
4 |
5 | ## Background
6 |
7 | This project contains the smart contracts for the Elixir Protocol integration on top of Vertex Protocol.
8 |
9 | See the [documentation](docs/docs.md), the [Elixir Protocol documentation](https://docs.elixir.finance/), and the [Vertex Protocol documentation](https://vertex-protocol.gitbook.io/docs/) for more information.
10 |
11 | ## Deployments
12 |
13 |
14 |
15 |
16 | Network |
17 | VertexManager |
18 | Distributor |
19 | Router WBTC (ID 1) |
20 | Router BTC-PERP (ID 2) |
21 | Router WETH (ID 3) |
22 | Router ETH-PERP (ID 4) |
23 | Router ARB (ID 5) |
24 | Router ARB-PERP (ID 6) |
25 | Router BNB-PERP (ID 8) |
26 | Router XRP-PERP (ID 10) |
27 | Router SOL-PERP (ID 12) |
28 | Router MATIC-PERP (ID 14) |
29 | Router SUI-PERP (ID 16) |
30 | Router OP-PERP (ID 18) |
31 | Router APT-PERP (ID 20) |
32 | Router LTC-PERP (ID 22) |
33 | Router BCH-PERP (ID 24) |
34 | Router COMP-PERP (ID 26) |
35 | Router MKR-PERP (ID 28) |
36 | Router MPEPE-PERP (ID 30) |
37 | Router USDT (ID 31) |
38 | Router DOGE-PERP (ID 34) |
39 | Router LINK-PERP (ID 36) |
40 | Router DYDX-PERP (ID 38) |
41 | Router CRV-PERP (ID 40) |
42 | Router VRTX (ID 41) |
43 | Router TIA-PERP (ID 44) |
44 | Router PYTH-PERP (ID 46) |
45 | Router MBONK-PERP (ID 48) |
46 | Router JTO-PERP (ID 50) |
47 | Router AVAX-PERP (ID 52) |
48 | Router INJ-PERP (ID 54) |
49 | Router SNX-PERP (ID 56) |
50 | Router ADA-PERP (ID 58) |
51 | Router IMX-PERP (ID 60) |
52 | Router MEME-PERP (ID 62) |
53 |
54 |
55 | Arbitrum Mainnet |
56 | 0x052Ab3fd33cADF9D9f227254252da3f996431f75 |
57 | 0xe3e3A6cF662a6d7b2B8A60E8aE44636C7E014476 |
58 | 0x5E5E03AaE77C667664bA47556528a947af0A4716 |
59 | 0xA760E3dF6026a462A81EEe0227921D156d94C888 |
60 | 0x86612c5C2bdAe1e8534778B6C9C5535f635Fd04e |
61 | 0x5328277109AdE587C69B90e2D6BDD004A97E1bB9 |
62 | 0x8294Ea1bdAac220B6b840B6F9d294aDf6cD069aD |
63 | 0xE2F852E5877fD6901481c6f5bb2ecD94919ba026 |
64 | 0xCE30817dB0106b0362f3310ABD43fD0623Be83D7 |
65 | 0x8e7C90103e86Ba0171c3c37F84cCdB19B93b2C62 |
66 | 0x2DCa8aB151811D7425446931Cb138072bD815DCD |
67 | 0x16e1c7beCdD3bD7171AceD6f0774e076a1a3Ccd6 |
68 | 0xF967Db12dc3eAA2bFd5958b33D3F4c787cD01394 |
69 | 0x3DfE28737C7fD444111cA30d521B75f9b0C803E7 |
70 | 0x3421bb71E71919A2a2809D1Ec3A2DFcFd8eEd890 |
71 | 0xFfF7a80Fcb3ade0379bd09B50f8dda9adcA3e17d |
72 | 0x7805db7765a61Ec70D94A262ca7F46ce2A0Cf85F |
73 | 0xA5205f83dE3D66674635Ac9642464ee6b169E5ff |
74 | 0xeAc3A369FBe6C44a137ff6Fb5dE771c1891a201E |
75 | 0xC61f8e36E763a645BbA417A3d88c1A2DDe62faa0 |
76 | 0xEe7DFBe0CE3ad8044eB36C38bDb59f56e0f86088 |
77 | 0x4662Ed14d509791A5a1Fe0376415a2A8438bd53a |
78 | 0x5B4F6c8527237038d922a9f9cC7726bE65E7f27a |
79 | 0xf06d2fd349Fc5B4BEA2F4Ac2997A8F21C1b5d025 |
80 | 0xaA19B0EC4a0E97d202B04713Ac76853Abd3dd2dA |
81 | 0x978e93303f34B06e6D23C69919eD78Bb58C5A5C1 |
82 | 0x8a55474125ffF3b0EcF22cCCBf6a3D136472B15c |
83 | 0xfF5055A951c45F699c869E415378CF7d8d2fd81A |
84 | 0x4ee684B4a9b6F5db3f68Cbf0490B5Dd7A9C575A9 |
85 | 0x4F2442e93F6759d6F0F267c00E442eb2Da0Ac609 |
86 | 0x4f4C0Cb268b22E033361F76D63b031f0Bc4489d7 |
87 | 0x86A3DE1b2CfB34cCb604dB1ca4217255E699E8d3 |
88 | 0x33FC7F79cdE6620C64354ff63cd0B7C11C421f01 |
89 | 0x1EaCB7801517f45Ab7A8714eD91B6B28CfFe842A |
90 | 0x7625866Ab6f11809b2fdE3bF79f81780D6323E3b |
91 | 0xE034469069eba2Fa87514616640c3934B8975c2B |
92 |
93 |
94 | Arbitrum Sepolia |
95 | 0x052Ab3fd33cADF9D9f227254252da3f996431f75 |
96 | 0xe3e3A6cF662a6d7b2B8A60E8aE44636C7E014476 |
97 | 0x5E5E03AaE77C667664bA47556528a947af0A4716 |
98 | 0xA760E3dF6026a462A81EEe0227921D156d94C888 |
99 | 0x86612c5C2bdAe1e8534778B6C9C5535f635Fd04e |
100 | 0x5328277109AdE587C69B90e2D6BDD004A97E1bB9 |
101 | 0x8294Ea1bdAac220B6b840B6F9d294aDf6cD069aD |
102 | 0xE2F852E5877fD6901481c6f5bb2ecD94919ba026 |
103 | 0xCE30817dB0106b0362f3310ABD43fD0623Be83D7 |
104 | 0x8e7C90103e86Ba0171c3c37F84cCdB19B93b2C62 |
105 | 0x2DCa8aB151811D7425446931Cb138072bD815DCD |
106 | 0x16e1c7beCdD3bD7171AceD6f0774e076a1a3Ccd6 |
107 | 0xF967Db12dc3eAA2bFd5958b33D3F4c787cD01394 |
108 | 0x3DfE28737C7fD444111cA30d521B75f9b0C803E7 |
109 | 0x3421bb71E71919A2a2809D1Ec3A2DFcFd8eEd890 |
110 | 0xFfF7a80Fcb3ade0379bd09B50f8dda9adcA3e17d |
111 | 0x7805db7765a61Ec70D94A262ca7F46ce2A0Cf85F |
112 | 0xA5205f83dE3D66674635Ac9642464ee6b169E5ff |
113 | 0xeAc3A369FBe6C44a137ff6Fb5dE771c1891a201E |
114 | 0xC61f8e36E763a645BbA417A3d88c1A2DDe62faa0 |
115 | 0xEe7DFBe0CE3ad8044eB36C38bDb59f56e0f86088 |
116 | 0x4662Ed14d509791A5a1Fe0376415a2A8438bd53a |
117 | 0x5B4F6c8527237038d922a9f9cC7726bE65E7f27a |
118 | 0xf06d2fd349Fc5B4BEA2F4Ac2997A8F21C1b5d025 |
119 | 0xaA19B0EC4a0E97d202B04713Ac76853Abd3dd2dA |
120 | 0x978e93303f34B06e6D23C69919eD78Bb58C5A5C1 |
121 | 0x8a55474125ffF3b0EcF22cCCBf6a3D136472B15c |
122 | 0xfF5055A951c45F699c869E415378CF7d8d2fd81A |
123 | 0x4ee684B4a9b6F5db3f68Cbf0490B5Dd7A9C575A9 |
124 | 0x4F2442e93F6759d6F0F267c00E442eb2Da0Ac609 |
125 | 0x4f4C0Cb268b22E033361F76D63b031f0Bc4489d7 |
126 | 0x86A3DE1b2CfB34cCb604dB1ca4217255E699E8d3 |
127 | 0x33FC7F79cdE6620C64354ff63cd0B7C11C421f01 |
128 | 0x1EaCB7801517f45Ab7A8714eD91B6B28CfFe842A |
129 | 0x7625866Ab6f11809b2fdE3bF79f81780D6323E3b |
130 | 0xE034469069eba2Fa87514616640c3934B8975c2B |
131 |
132 |
133 |
134 | ## Documentation
135 |
136 | You can find the technical documentation and references of the smart contracts [here](docs/docs.md).
137 |
138 | ## Usage
139 |
140 | You will need a copy of [Foundry](https://github.com/foundry-rs/foundry) installed before proceeding. See the [installation guide](https://github.com/foundry-rs/foundry#installation) for details.
141 |
142 | To build the contracts:
143 |
144 | ```sh
145 | git clone https://github.com/ElixirProtocol/vertex-contracts.git
146 | cd vertex-contracts
147 | forge install
148 | forge build
149 | ```
150 |
151 | ### Run Tests
152 |
153 | In order to run unit tests, run:
154 |
155 | ```sh
156 | forge test
157 | ```
158 |
159 | For longer fuzz campaigns, run:
160 |
161 | ```sh
162 | FOUNDRY_PROFILE="deep" forge test
163 | ```
164 |
165 | ### Run Slither
166 |
167 | After [installing Slither](https://github.com/crytic/slither#how-to-install), run:
168 |
169 | ```sh
170 | slither src/
171 | ```
172 |
173 | ### Check coverage
174 |
175 | To check the test coverage, run:
176 |
177 | ```sh
178 | forge coverage
179 | ```
180 |
181 | ### Update Gas Snapshots
182 |
183 | To update the gas snapshots, run:
184 |
185 | ```sh
186 | forge snapshot
187 | ```
188 |
189 | ### Deploy Contracts
190 |
191 | In order to deploy the contracts, set the relevant constants in the respective chain script, and run the following command(s):
192 |
193 | ```sh
194 | forge script script/deploy/DeploySepolia.s.sol:DeploySepolia -vvvv --fork-url RPC --broadcast --slow
195 | ```
196 |
--------------------------------------------------------------------------------
/defender.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "contract_inspector": {
3 | "scan_directories": ["src"]
4 | }
5 | }
--------------------------------------------------------------------------------
/docs/docs.md:
--------------------------------------------------------------------------------
1 | # Elixir <> Vertex Documentation
2 |
3 | Overview of Elixir's smart contract architecture integrating to Vertex Protocol.
4 |
5 | ## Table of Contents
6 |
7 | - [Background](#background)
8 | - [Overview](#overview)
9 | - [Sequence of Events](#sequence-of-events)
10 | - [Lifecycle](#example-lifecycle-journey)
11 | - [Incident Response & Monitoring](#incident-response--monitoring)
12 | - [Aspects](#aspects)
13 |
14 | ## Background
15 |
16 | Elixir is building the industry's decentralized, algorithmic market-making protocol. The protocol algorithmically deploys supplied liquidity on the order books, utilizing the equivalent of x*y=k curves to build liquidity and tighten the bid/ask spread. The protocol provides crucial decentralized infrastructure, allowing exchanges and protocols to easily bootstrap liquidity to their books. It also enables crypto projects to incentivize liquidity to their centralized exchange pairs via LP tokens.
17 |
18 | This repository contains the smart contracts to power the first native integration between Elixir and Vertex, a cross-margined DEX protocol offering spot, perpetuals, and an integrated money market bundled into one vertical application on Arbitrum. Vertex is powered by a hybrid unified central limit order book (CLOB) and integrated automated market maker (AMM). Gas fees and MEV are minimized on Vertex due to the batched transaction and optimistic rollup model of the underlying Arbitrum Layer 2, where Vertex's smart contracts control the risk engine and core products.
19 |
20 | This integration aims to unlock retail liquidity for algorithmic market-making on Vertex. Due to the nature of the underlying integration, the Elixir smart contracts have unique features and functions that allow building on top of it. For example, an important aspect of Vertex Protocol is their orderbook, a "sequencer" that operates as an off-chain node layered on top of their smart contracts and contained within the Arbitrum protocol layer.
21 |
22 | More information:
23 | - [Elixir Protocol Documentation](https://docs.elixir.finance/)
24 | - [Vertex Protocol Dcoumentation](https://vertex-protocol.gitbook.io/docs/)
25 |
26 | ## Overview
27 |
28 | This integration comprises two Elixir smart contracts a singleton (VertexManager) and a router (VertexRouter). VertexManager allows users to deposit and withdraw liquidity for spot and perpetual (perp) products on Vertex. By depositing liquidity, users earn VRTX rewards from the market-making done by the Elixir validator network off-chain. Rewards, denominated in the VRTX token, are distributed via epochs lasting 28 days and serve as the sustainability of the APRs. These rewards are deposited into the Distributor contract which allows users to claim tokens with signatures from the Elixir validator network. On the VertexManager smart contract, each Vertex product is associated with a pool structure, which contains a type and router (VertexRouter), plus a nested mapping containing the data of supported tokens in that pool. As Vertex contains their own sequencer, the data from their contracts on-chain is lagging. Therefore, the VertexManager contract implements a FIFO queue so that the Elixir sequencer can process deposits and withdrawals using the latest data off-chain. On the other hand, VertexRouter allows to have one linked signer per VertexManager pool — linked signers let the off-chain Elixir network market make on behalf of the pools. Regarding Vertex, the Elixir smart contract interacts mainly with the Endpoint and Clearinghouse smart contracts.
29 |
30 | - [VertexManager](src/VertexManager.sol): Elixir smart contract to deposit, withdraw, claim and manage product pools.
31 | - [VertexRouter](src/VertexRouter.sol): Elixir smart contract to route slow-mode transactions to Vertex, on behalf of a VertexManager pool.
32 | - [Distributor](src/Distributor.sol): Elixir smart contract to claim rewards.
33 | - [Endpoint](https://github.com/vertex-protocol/vertex-contracts/blob/main/contracts/Endpoint.sol): Vertex smart contract that serves as the entry point for all actions and interactions with their protocol.
34 | - [Clearinghouse](https://github.com/vertex-protocol/vertex-contracts/blob/main/contracts/Clearinghouse.sol): Vertex smart contract that serves as the clearinghouse for all trades and positions, storing the liquidity (TVL) of their protocol. Only used in VertexManager initialization to fetch the fee token address.
35 |
36 | ## Sequence of Events
37 |
38 | ### Deposit Liquidity
39 | Liquidity for spot and perp pools can be deposited into the VertexManager smart contract by calling the `depositSpot` and `depositPerp` functions, respectively.
40 |
41 | Every spot product is composed of two tokens, the base token and the quote token, which is usually USDC. For this reason, liquidity for a spot product has to be deposited in a balanced way, meaning that the amount of base and quote tokens have to be equal in value. To enfore this, deposits are queued and processed by the Elixir. When depositing into spot pools, the user specifies a slippage amount, which is checked when Elixir processes it. If the amount of tokens to deposit calculated by Elixir is outside the slippage range, the deposit is skipped. On the other hand, perp pools don't require balanced liquidity but Elixir processes them to calculate the amount of shares the user should receive. In order to process the queue, Elixir takes a fee in native ETH when depositing or withdrawing.
42 |
43 | The `depositSpot` flow is the following:
44 |
45 | 1. Check that deposits are not paused.
46 | 2. Check that the reentrancy guard is not active.
47 | 3. Check that the pool given is a spot one.
48 | 4. Check that the tokens given are two and are not duplicated.
49 | 5. Check that the receiver is not a zero address (for the good of the user).
50 | 6. Get the Elixir processing fee in native ETH
51 | 7. Queue the deposit.
52 |
53 | And the `depositPerp` flow is the following:
54 |
55 | 1. Check that deposits are not paused.
56 | 2. Check that the reentrancy guard is not active.
57 | 3. Check that the pool given is a perp one.
58 | 5. Check that the receiver is not a zero address (for the good of the user).
59 | 6. Get the Elixir processing fee in native ETH
60 | 7. Queue the deposit.
61 |
62 | Afterwards, the Elixir sequencer will call the `unqueue` function which processes the next transaction in the queue. For deposits, the process flow is the following:
63 |
64 | 1. If the pool is spot, check for slippage.
65 | 2. Call the `_deposit` function for every token being deposited (2 for spot, 1 for perp).
66 | 3. The `_deposit` function:
67 | - Checks that the token is supported by the pool.
68 | - Checks that the token amount to deposit will not exceed the liquidity hardcap of the token in the pool.
69 | - Transfer the tokens from the depositor to the smart contract.
70 | - Build and send the deposit transaction to Vertex, redirecting the received tokens to the Elixir account (EOA, established a linked signer) via the VertexRouter smart contract assigned to this pool.
71 | - Update the pool data and balances with the new amount.
72 | 4. Emit the `Deposit` event.
73 | 5. Update the queue state to mark this deposit as processed.
74 |
75 | > Note: Only tokens with less or equal to 18 decimals are supported by Vertex.
76 |
77 | ### Withdraw Liquidity
78 | In order to start a withdrawal, the user should call the `withdrawSpot` or `withdrawPerp` functions in order to signal to the Elixir network their intention of withdrawing. These withdraw functions will queue their intent, and the Elixir sequencer will process it by calling the `unqueue` function. To process a withdrawal, the Elixir sequencer burns the user's shares and sends a withdrawal request to Vertex, charging a fee of 1 USDC. When the request is processed by Vertex, the liquidity is transfered to the pool's VertexRouter smart contract. Here, the user needs to "manually" claim the liquidity via the `claim` function on the Elixir VertexManager smart contract, as no Vertex callback is available. Anyone can call this function on behalf of any address, allowing us to monitor pending claims and process them for users. When calling the `claim` function, the VertexManger smart contract will transfer the liquidity from the pool's VertexRouter into the VertexManager smart contract, which is then transferred to the user.
79 |
80 | The `withdrawSpot` flow is the following:
81 |
82 | 1. Check that withdrawals are not paused.
83 | 2. Check that the reentrancy guard is not active.
84 | 3. Check that the pool given is a spot one.
85 | 5. Check that the tokens are not duplicated.
86 | 6. Get the Elixir processing fee in native ETH
87 | 7. Queue the withdrawal.
88 |
89 | And the `withdrawPerp` flow is the following:
90 |
91 | 1. Check that withdrawals are not paused.
92 | 2. Check that the reentrancy guard is not active.
93 | 3. Check that the pool given is a perp one.
94 | 4. Get the Elixir processing fee in native ETH
95 | 5. Queue the withdrawal.
96 |
97 | Afterwards, the Elixir sequencer will call the `unqueue` function which processes the next transaction in the queue. For withdrawals, the process flow is the following:
98 |
99 | 1. Call the `_withdraw` function for every token being withdrawn (2 for spot, 1 for perp).
100 | 2. The `_withdraw` function:
101 | - Subtract the amount of tokens given from the user's balance on the pool data. Reverts if the user does not have enough balance.
102 | - Add fee amount to the balance of Elixir as it will pay the sequencer fee of 1 USDC on behalf of the user. Elixir is automatically reimbursed with user claims, but it can also reimburse itself by calling the `claim` function on behalf of a user.
103 | - Substract fee amount from the calculated token amount to withdraw and stored as the pending balance for claims afterward.
104 | - Build and send the withdrawal request to Vertex.
105 | 3. Emit the `Withdraw` event.
106 | 4. Update the queue state to mark this withdraw as processed.
107 |
108 | > Note: It's vital for the VertexManager smart contract to maintain the invariant of: "each pool must have a unique pool" or "two pools cannot share the same router." Otherwise, a pool with a wrong router will lead to loss of funds during withdrawals because of an inflated balance of the pools. By nature of the current logic, it's impossible for the invariant to break, as changing a pool's router is not supported.
109 |
110 | ### Claim Liquidity
111 | After the Vertex sequencer fulfills a withdrawal request, the funds will be available to claim on the VertexManager smart contract by calling the `claim` function. This function can be called by anyone on behalf of a user, allowing us to monitor the pending claims and process them for users. The flow of the `claim` function is the following:
112 |
113 | 1. Check that claims are not paused.
114 | 2. Check that the reentrancy guard is not active.
115 | 3. Check that the pool is valid (i.e., it exists).
116 | 4. Check that the user to claim for is not the zero address.
117 | 5. Fetch and store the pending balance of the user for the token.
118 | 6. Fetch and store the Elixir reimburse fee amount.
119 | 7. Reset the pending balance and fee to 0.
120 | 8. Transfer the token amount to the user.
121 | 9. Transfer the fee amount to the owner (Elixir).
122 | 10. Emit the `Claim` event.
123 |
124 | > Note: As pending balances are not stored sequentially, users are able to claim funds in any order as they arrive at the VertexRouter smart contract. This is expected behavior and does not affect the user's funds as the Vertex sequencer will continue to fulfill withdrawal requests, which can also be manually processed after days of inactivity by the Vertex sequencer.
125 |
126 | ### Reward Distribution
127 | By market-making (creating and filling orders on the Vertex order book), the Elixir validator network earns VRTX rewards. These rewards are distributed to the users who deposited liquidity on the VertexManager smart contract, depending on the amount and time of their liquidity. Rewards are distributed via epochs lasting 28 days and serve as the sustainability of the APRs. These rewards are deposited into the Distributor contract which allows users to claim tokens with signatures from the Elixir validator network.
128 |
129 | Learn more about the VRTX reward mechanism [here](https://vertex-protocol.gitbook.io/docs/community-token-and-dao/trading-rewards-detailed-mechanism).
130 |
131 | ## Example Lifecycle Journey
132 |
133 | ### Spot Product
134 |
135 | - A user approves the VertexManager smart contract to spend their tokens.
136 | - User calls `depositSpot` and passes the following parameters:
137 | * `id`: The ID of the pool to deposit to.
138 | * `token0`: The base token to deposit.
139 | * `token1`: The quote token to deposit.
140 | * `amount0`: The amount of base tokens to deposit.
141 | * `amount1Low`: The low limit of the quote amount.
142 | * `amount1High`: The high limit of the quote amount.
143 | * `receiver`: The receiver of the virtual LP balance.
144 | - Elixir processes the deposit from the queue. If the calculated amount of quote tokens is out of the given range (`amount1Low` <> `amount1High`), the function reverts due to slippage.
145 | - The `_deposit` redirects liquidity to Vertex and updates the LP balances, giving the shares to the receiver.
146 | - The Elixir network of decentralized validators receives the liquidity and market makes with it, generating VRTX rewards.
147 | - After some time, the user (i.e., receiver) calls the `withdrawSpot` function to initiate a withdrawal, passing the following parameters:
148 | * `id`: The ID of the pool to withdraw from.
149 | * `token0`: The base token to withdraw.
150 | * `token1`: The quote token to withdraw.
151 | * `amount0`: The amount of base tokens to withdraw.
152 | - Elixir processes the withdrawal from the queue.
153 | - The `_withdraw` function updates the LP balances and sends the withdrawal requests to Vertex.
154 | - After the Vertex sequencer fulfills the withdrawal request, the funds are available to be claimed via the `claim` function.
155 |
156 | ### Perpetual Product
157 |
158 | - A user approves the VertexManager smart contract to spend their tokens.
159 | - User calls `depositPerp` and passes the following parameters:
160 | * `id`: The ID of the pool to deposit to.
161 | * `token`: The token to deposit.
162 | * `amount`: The amount of tokens to deposit.
163 | * `receiver`: The receiver of the virtual LP balance.
164 | - Elixir processes the deposit from the queue.
165 | - The `_deposit` redirects liquidity to Vertex and updates the LP balances, giving the shares to the receiver.
166 | - The Elixir network of decentralized validators receives the liquidity and market makes with it, generating VRTX rewards.
167 | - After some time, the user (i.e., receiver) calls the `withdrawPerp` function to initiate a withdrawal, passing the following parameters:
168 | * `id`: The ID of the pool to withdraw from.
169 | * `token`: The token to withdraw.
170 | * `amount`: The amount of tokens to withdraw.
171 | - Elixir processes the withdrawal from the queue.
172 | - The `_withdraw` function updates the LP balances and sends the withdrawal requests to Vertex.
173 | - After the Vertex sequencer fulfills the withdrawal request, the funds are available to be claimed via the `claim` function.
174 |
175 | ## Incident Response & Monitoring
176 |
177 | The Elixir team is planning to protect the smart contracts with Chainalysis Incident Response (CIR) in the event of a hack or exploit. The benefits of CIR include:
178 |
179 | - CIR helps deter hackers by letting them know a leading global crypto investigative team is on our side.
180 | - With CIR, we can tap into Chainalysis’ expertise for complex blockchain analysis and investigations. The CIR team is ready to respond to cybersecurity breaches, ransomware attacks, recovery of stolen cryptocurrency, and perform other analyses involving blockchain data. The team consists of respected professional investigators, cybersecurity experts, and data engineers.
181 | - Having a proactive solution in place decreases the time to respond and increases the likelihood of asset freezing and recovery or law enforcement should the worst happen.
182 | - The ability to trace funds through various types of complex platforms is a crucial part of the CIR incident response and the ability to recover funds successfully. This applies to identified mixer platforms but also unidentified mixers and new bridging protocols between blockchains.
183 | - Chainalysis has a huge customer base and, with it, a sizable network with personal connections to almost all significant exchanges and services in the crypto space. Also, their strong relationship with Law Enforcement Agencies around the world makes them very efficient in engaging the relevant entities when needed.
184 | - In over 80% of all cases where an incident has occurred, Chainalysis investigators have been able to give our customers valuable information that leads to recovery of more than what their CIR fee was.
185 |
186 | ## General Aspects
187 |
188 | ### Vertex Integration
189 |
190 | Because Vertex does not offer an updated state of an account when there are transactions affecting them inside the sequencer queue, the protocol works around this by using an off-chain sequencer to calculate values for deposits and withdrawals.
191 |
192 | ### Authentication / Access Control
193 |
194 | Appropiate access controls are in place for all priviliged operations. The only privliged role in the smart contract is the owner, which is the Elixir multisig. As the Vertex protocol is completely upgradeable, the owner role is needed to perform operations and actions in case any aspect needs to be updated. The capabilities of the owner are the following:
195 |
196 | - `pause`: Update the pause status of deposits, withdraws, and claims in case of malicious activity or incidents. Allows to pause each operation modularly; for example, pause deposits but allow withdrawals and claims.
197 | - `addPool`: Adds a new pool. This deploys a new router for the pool, which is unique for this pool. The reason is that Vertex only allows one linked signer per smart contract, which goes against the singleton design of the Manager contract.
198 | - `addPoolTokens`: Adds a new token to a pool.
199 | - `updatePoolHardcaps`: Update the hardcaps of a pool. Used to limit and manage market making activity on Vertex for scaling purposes. An alternative to pausing deposits too.
200 | - `updateToken`: Update the Vertex product ID of a token address. Used when new tokens are supported on Vertex products.
201 |
--------------------------------------------------------------------------------
/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | src = 'src' # The source directory
3 | out = 'out' # The output directory
4 | libs = ['lib'] # A list of library directories
5 | optimizer = true # Enable or disable the solc optimizer
6 | optimizer_runs = 200 # The number of optimizer runs
7 | fs_permissions = [{ access = "read", path = "./"}] # Gives permission to read files for deployment keys.
8 |
9 | [fuzz]
10 | runs = 100 # The number of times to run the fuzzing tests
11 |
12 | [invariant]
13 | runs = 8 # The number of calls to make in the invariant tests
14 | depth = 8 # The number of times to run the invariant tests
15 | fail_on_revert = true # Fail the test if the contract reverts
16 |
17 | [profile.shallow.fuzz]
18 | runs = 20 # The number of times to run the fuzzing tests
19 |
20 | [profile.deep.fuzz]
21 | runs = 150 # The number of times to run the fuzzing tests
22 |
23 | [profile.deep.invariant]
24 | runs = 12 # The number of times to run the invariant tests
25 | depth = 12 # The number of calls to make in the invariant tests
26 |
27 | [profile.super_deep.fuzz]
28 | runs = 500 # The number of times to run the fuzzing tests
29 |
30 | [profile.super_deep.invariant]
31 | runs = 16 # The number of calls to make in the invariant tests
32 | depth = 16 # The number of times to run the invariant tests
33 |
34 | [rpc_endpoints]
35 | arbitrum = "${ARBITRUM_RPC_URL}"
36 | sepolia = "${SEPOLIA_RPC_URL}"
--------------------------------------------------------------------------------
/remappings.txt:
--------------------------------------------------------------------------------
1 | forge-std/=lib/forge-std/src/
2 | ds-test/=lib/forge-std/lib/ds-test/src/
3 | openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
4 | openzeppelin/=lib/openzeppelin-contracts/contracts/
--------------------------------------------------------------------------------
/script/USDC.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Script.sol";
5 |
6 | import {VertexManager, IEndpoint} from "src/VertexManager.sol";
7 | import {VertexProcessor} from "src/VertexProcessor.sol";
8 | import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
9 |
10 | // Scrip to migrate from USDC.e to USDC (Sepolia example).
11 | contract USDC is Script {
12 | VertexManager internal manager;
13 |
14 | IERC20Metadata USDC = IERC20Metadata(0xD32ea1C76ef1c296F131DD4C5B2A0aac3b22485a);
15 |
16 | function run() external {
17 | // Start broadcast.
18 | vm.startBroadcast(vm.envUint("KEY"));
19 |
20 | // Wrap in ABI to support easier calls.
21 | manager = VertexManager(0x052Ab3fd33cADF9D9f227254252da3f996431f75);
22 |
23 | // Get the endpoint address.
24 | IEndpoint endpoint = manager.endpoint();
25 |
26 | /*//////////////////////////////////////////////////////////////
27 | STEP 1
28 | //////////////////////////////////////////////////////////////*/
29 |
30 | // Pause the manager.
31 | manager.pause(true, true, true);
32 |
33 | /*//////////////////////////////////////////////////////////////
34 | STEP 2
35 | //////////////////////////////////////////////////////////////*/
36 |
37 | // Upgrade to new version.
38 |
39 | // Deploy new Processor implementation.
40 | VertexProcessor newProcessor = new VertexProcessor();
41 |
42 | // Deploy new Manager implementation.
43 | VertexManager newManager = new VertexManager();
44 |
45 | // Upgrade proxy to new implementation.
46 | manager.upgradeToAndCall(
47 | address(newManager), abi.encodeWithSelector(VertexManager.updateProcessor.selector, address(newProcessor))
48 | );
49 |
50 | // Check upgrade by ensuring storage is not changed.
51 | require(address(manager.endpoint()) == address(endpoint), "Invalid upgrade");
52 |
53 | /*//////////////////////////////////////////////////////////////
54 | STEP 3
55 | //////////////////////////////////////////////////////////////*/
56 |
57 | // Add USDC token to all pools. This is needed so that all of the routers approve this new token to transfer in and out.
58 | address[] memory token = new address[](1);
59 | token[0] = address(USDC);
60 |
61 | uint256[] memory hardcap = new uint256[](1);
62 | hardcap[0] = 0;
63 |
64 | // Count to check all pools.
65 | uint256 count;
66 |
67 | // 1-6
68 | for (uint256 i = 1; i <= 6; i++) {
69 | manager.addPoolTokens(i, token, hardcap);
70 | count++;
71 | }
72 |
73 | // 8-30, 2 steps
74 | for (uint256 i = 8; i <= 30; i += 2) {
75 | manager.addPoolTokens(i, token, hardcap);
76 | count++;
77 | }
78 |
79 | manager.addPoolTokens(31, token, hardcap);
80 | count++;
81 |
82 | // 34-40, 2 steps
83 | for (uint256 i = 34; i <= 40; i += 2) {
84 | manager.addPoolTokens(i, token, hardcap);
85 | count++;
86 | }
87 |
88 | manager.addPoolTokens(44, token, hardcap);
89 | count++;
90 |
91 | manager.addPoolTokens(46, token, hardcap);
92 | count++;
93 |
94 | // 50-62, 2 steps
95 | for (uint256 i = 50; i <= 62; i += 2) {
96 | manager.addPoolTokens(i, token, hardcap);
97 | count++;
98 | }
99 |
100 | require(count == 32, "Missing pools");
101 |
102 | /*//////////////////////////////////////////////////////////////
103 | STEP 4
104 | //////////////////////////////////////////////////////////////*/
105 |
106 | // Update the quote token to use the new USDC token and store the previous one (USDC.e)
107 | manager.updateQuoteToken(address(USDC));
108 |
109 | /*//////////////////////////////////////////////////////////////
110 | STEP 5
111 | //////////////////////////////////////////////////////////////*/
112 |
113 | // Owner (multisig in mainnet, EOA in testnet) should approve USDC for slow-mode fee and make sure to have enough (for exmaple, swapping USDC.e to USDC)
114 | USDC.approve(address(manager), type(uint256).max);
115 | // deal(address(USDC), address(manager.owner()), 10000000 * 10 ** USDC.decimals());
116 |
117 | /*//////////////////////////////////////////////////////////////
118 | STEP 6
119 | //////////////////////////////////////////////////////////////*/
120 |
121 | // Unpause manager
122 | manager.pause(false, false, false);
123 | }
124 |
125 | // Exclude from coverage report
126 | function test() public {}
127 | }
128 |
--------------------------------------------------------------------------------
/script/Upgrade.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Script.sol";
5 |
6 | import {VertexManager} from "src/VertexManager.sol";
7 |
8 | contract UpgradeContract is Script {
9 | VertexManager internal manager;
10 | VertexManager internal newManager;
11 |
12 | function run() external {
13 | // Start broadcast.
14 | vm.startBroadcast();
15 |
16 | // Wrap in ABI to support easier calls.
17 | manager = VertexManager(0x052Ab3fd33cADF9D9f227254252da3f996431f75);
18 |
19 | // Get the endpoint address before upgrading.
20 | address endpoint = address(manager.endpoint());
21 |
22 | // Deploy new implementation.
23 | newManager = new VertexManager();
24 |
25 | // Upgrade proxy to new implementation.
26 | manager.upgradeTo(address(newManager));
27 |
28 | uint256[] memory pools = new uint256[](2);
29 | address[] memory signers = new address[](2);
30 |
31 | pools[0] = 38;
32 | pools[1] = 40;
33 |
34 | signers[0] = 0x28CcdB531854d09D48733261688dc1679fb9A242;
35 | signers[1] = 0x28CcdB531854d09D48733261688dc1679fb9A242;
36 |
37 | manager.updateLinkedSigners(pools, signers);
38 | vm.stopBroadcast();
39 |
40 | // Check upgrade by ensuring storage is not changed.
41 | require(address(manager.endpoint()) == endpoint, "Invalid upgrade");
42 | }
43 |
44 | // Exclude from coverage report
45 | function test() public {}
46 | }
47 |
--------------------------------------------------------------------------------
/script/deploy/DeployBase.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Script.sol";
5 |
6 | import {IEndpoint, VertexManager, IVertexManager} from "src/VertexManager.sol";
7 | import {ERC1967Proxy} from "openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
8 | import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
9 |
10 | abstract contract DeployBase is Script {
11 | // Environment specific variables.
12 | IEndpoint public endpoint;
13 | address public externalAccount;
14 | address public btc;
15 | address public usdc;
16 | address public eth;
17 | address public arb;
18 | address public usdt;
19 | address public vrtx;
20 |
21 | // Deploy addresses.
22 | VertexManager internal managerImplementation;
23 | ERC1967Proxy internal proxy;
24 | VertexManager internal manager;
25 |
26 | constructor(
27 | address _endpoint,
28 | address _externalAccount,
29 | address _btc,
30 | address _usdc,
31 | address _eth,
32 | address _arb,
33 | address _usdt,
34 | address _vrtx
35 | ) {
36 | endpoint = IEndpoint(_endpoint);
37 | externalAccount = _externalAccount;
38 | btc = _btc;
39 | usdc = _usdc;
40 | eth = _eth;
41 | arb = _arb;
42 | usdt = _usdt;
43 | vrtx = _vrtx;
44 | }
45 |
46 | function setup() internal {
47 | // Get the token decimals.
48 | uint256 btcDecimals = IERC20Metadata(btc).decimals();
49 | uint256 usdcDecimals = IERC20Metadata(usdc).decimals();
50 | uint256 ethDecimals = IERC20Metadata(eth).decimals();
51 | uint256 arbDecimals = IERC20Metadata(arb).decimals();
52 | uint256 usdtDecimals = IERC20Metadata(usdt).decimals();
53 | uint256 vrtxDecimals = IERC20Metadata(vrtx).decimals();
54 |
55 | // Start broadcast.
56 | vm.startBroadcast();
57 |
58 | // Deploy Factory implementation.
59 | managerImplementation = new VertexManager();
60 |
61 | // Deploy and initialize the proxy contract.
62 | proxy = new ERC1967Proxy(
63 | address(managerImplementation),
64 | abi.encodeWithSignature("initialize(address,uint256)", address(endpoint), 1000000)
65 | );
66 |
67 | // Wrap in ABI to support easier calls.
68 | manager = VertexManager(address(proxy));
69 |
70 | // Add token support.
71 | manager.updateToken(usdc, 0);
72 | manager.updateToken(btc, 1);
73 | manager.updateToken(eth, 3);
74 | manager.updateToken(arb, 5);
75 | manager.updateToken(usdt, 31);
76 | manager.updateToken(vrtx, 41);
77 |
78 | // Give approval to create pools.
79 | IERC20Metadata(usdc).approve(address(manager), type(uint256).max);
80 |
81 | // Spot BTC: WBTC and USDC
82 | address[] memory spotBTC = new address[](2);
83 | spotBTC[0] = btc;
84 | spotBTC[1] = usdc;
85 |
86 | uint256[] memory spotBTCHardcaps = new uint256[](2);
87 | spotBTCHardcaps[0] = 1371 * 10 ** btcDecimals - 2; // 13.71 WBTC
88 | spotBTCHardcaps[1] = 375000 * 10 ** usdcDecimals; // 375000 USDC
89 |
90 | manager.addPool(1, spotBTC, spotBTCHardcaps, IVertexManager.PoolType.Spot, externalAccount);
91 |
92 | // Perp BTC: USDC
93 | address[] memory singleUSDC = new address[](1);
94 | singleUSDC[0] = usdc;
95 |
96 | uint256[] memory perpBTCHardcaps = new uint256[](1);
97 | perpBTCHardcaps[0] = 375000 * 10 ** usdcDecimals; // 375000 USDC
98 |
99 | manager.addPool(2, singleUSDC, perpBTCHardcaps, IVertexManager.PoolType.Perp, externalAccount);
100 |
101 | // Spot ETH: ETH and USDC
102 | address[] memory spotETH = new address[](2);
103 | spotETH[0] = eth;
104 | spotETH[1] = usdc;
105 |
106 | uint256[] memory spotETHHardcaps = new uint256[](2);
107 | spotETHHardcaps[0] = 227 * 10 ** ethDecimals; // 227 ETH
108 | spotETHHardcaps[1] = 375000 * 10 ** usdcDecimals; // 375000 USDC
109 |
110 | manager.addPool(3, spotETH, spotETHHardcaps, IVertexManager.PoolType.Spot, externalAccount);
111 |
112 | // Perp ETH: USDC
113 | manager.addPool(4, singleUSDC, perpBTCHardcaps, IVertexManager.PoolType.Perp, externalAccount);
114 |
115 | // Spot ARB: ARB and USDC
116 | address[] memory spotARB = new address[](2);
117 | spotARB[0] = arb;
118 | spotARB[1] = usdc;
119 |
120 | uint256[] memory spotARBHardcaps = new uint256[](2);
121 | spotARBHardcaps[0] = 73500 * 10 ** arbDecimals; // 73500 ARB
122 | spotARBHardcaps[1] = 75000 * 10 ** usdcDecimals; // 75000 USDC
123 |
124 | manager.addPool(5, spotARB, spotARBHardcaps, IVertexManager.PoolType.Spot, externalAccount);
125 |
126 | // Perp ARB: USDC
127 | uint256[] memory perpHardcaps = new uint256[](1);
128 | perpHardcaps[0] = 90000 * 10 ** usdcDecimals; // 90000 USDC
129 |
130 | manager.addPool(6, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
131 |
132 | // Perp BNB: USDC
133 | manager.addPool(8, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
134 |
135 | // Perp XRP: USDC
136 | manager.addPool(10, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
137 |
138 | // Perp SOL: USDC
139 | manager.addPool(12, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
140 |
141 | // Perp MATIC: USDC
142 | manager.addPool(14, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
143 |
144 | // Perp SUI: USDC
145 | manager.addPool(16, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
146 |
147 | // Perp OP: USDC
148 | manager.addPool(18, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
149 |
150 | // Perp APT: USDC
151 | manager.addPool(20, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
152 |
153 | // Perp LTC: USDC
154 | manager.addPool(22, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
155 |
156 | // Perp BCH: USDC
157 | manager.addPool(24, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
158 |
159 | // Perp COMP: USDC
160 | manager.addPool(26, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
161 |
162 | // Perp MKR: USDC
163 | manager.addPool(28, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
164 |
165 | // Perp mPEPE: USDC
166 | manager.addPool(30, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
167 |
168 | // Spot USDT: USDT and USDC
169 | address[] memory spotUSDT = new address[](2);
170 | spotUSDT[0] = usdt;
171 | spotUSDT[1] = usdc;
172 |
173 | uint256[] memory spotUSDTHardcaps = new uint256[](2);
174 | spotUSDTHardcaps[0] = 45000 * 10 ** usdtDecimals; // 45000 USDT
175 | spotUSDTHardcaps[1] = 45000 * 10 ** usdcDecimals; // 45000 USDC
176 |
177 | manager.addPool(31, spotUSDT, spotUSDTHardcaps, IVertexManager.PoolType.Spot, externalAccount);
178 |
179 | // Perp DOGE: USDC
180 | manager.addPool(34, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
181 |
182 | // Perp LINK: USDC
183 | manager.addPool(36, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
184 |
185 | // Perp DYDX: USDC
186 | manager.addPool(38, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
187 |
188 | // Perp CRV: USDC
189 | manager.addPool(40, singleUSDC, perpHardcaps, IVertexManager.PoolType.Perp, externalAccount);
190 |
191 | // Spot VRTX: VRTX and USDC
192 | address[] memory spotVRTX = new address[](2);
193 | spotVRTX[0] = btc;
194 | spotVRTX[1] = usdc;
195 |
196 | uint256[] memory spotVRTXHardcaps = new uint256[](2);
197 | spotVRTXHardcaps[0] = 0;
198 | spotVRTXHardcaps[1] = 0;
199 |
200 | manager.addPool(41, spotVRTX, spotVRTXHardcaps, IVertexManager.PoolType.Spot, externalAccount);
201 |
202 | vm.stopBroadcast();
203 | }
204 |
205 | // Exclude from coverage report
206 | function test() public virtual {}
207 | }
208 |
--------------------------------------------------------------------------------
/script/deploy/DeployMainnet.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Script.sol";
5 | import {DeployBase} from "script/deploy/DeployBase.s.sol";
6 | import {VertexManager} from "src/VertexManager.sol";
7 |
8 | contract DeployMainnet is DeployBase {
9 | // Vertex Mainnet Endpoint
10 | constructor()
11 | DeployBase(
12 | 0xbbEE07B3e8121227AfCFe1E2B82772246226128e,
13 | 0xD7cb7F791bb97A1a8B5aFc3aec5fBD0BEC4536A5,
14 | 0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f,
15 | 0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8,
16 | 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1,
17 | 0x912CE59144191C1204E64559FE8253a0e49E6548,
18 | 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9,
19 | 0x95146881b86B3ee99e63705eC87AfE29Fcc044D9
20 | )
21 | {}
22 |
23 | function run() external {
24 | setup();
25 |
26 | vm.startBroadcast();
27 |
28 | // Transfer ownership to multisig
29 | manager.transferOwnership(0xdc91701CD5d5a3Adb34d9afD1756f63d3b2201Ac);
30 |
31 | vm.stopBroadcast();
32 | }
33 |
34 | // Exclude from coverage report
35 | function test() public override {}
36 | }
37 |
--------------------------------------------------------------------------------
/script/deploy/DeploySepolia.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Script.sol";
5 | import {DeployBase} from "script/deploy/DeployBase.s.sol";
6 | import {VertexManager} from "src/VertexManager.sol";
7 | import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
8 |
9 | contract DeploySepolia is DeployBase {
10 | // Vertex Sepolia Endpoint
11 | constructor()
12 | DeployBase(
13 | 0xaDeFDE1A14B6ba4DA3e82414209408a49930E8DC,
14 | 0x28CcdB531854d09D48733261688dc1679fb9A242,
15 | 0xA7Fcb606611358afa388b6bd23b3B2F2c6abEd82,
16 | 0xbC47901f4d2C5fc871ae0037Ea05c3F614690781,
17 | 0x94B3173E0a23C28b2BA9a52464AC24c2B032791c,
18 | 0x0881FAabdDdECf1B4c3D5331DF33C13A1b6589ea,
19 | 0xA1c062ddEf8f7B0a97e3Bb219108Ce73410772cE,
20 | 0x00aBCa5597d51e6C06eCfA655E73CE70A1e2cdCf
21 | )
22 | {}
23 |
24 | function run() external {
25 | setup();
26 | }
27 |
28 | // Exclude from coverage report
29 | function test() public override {}
30 | }
31 |
--------------------------------------------------------------------------------
/script/pools/AddBase.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Script.sol";
5 |
6 | import {VertexManager, IVertexManager} from "src/VertexManager.sol";
7 | import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
8 |
9 | contract AddPool is Script {
10 | // Environment specific variables.
11 | IERC20Metadata public base;
12 | VertexManager public manager;
13 | address public externalAccount;
14 | uint32 public productId;
15 | address public token;
16 | address[] public tokens;
17 | uint256[] public hardcaps;
18 | bool public spot;
19 |
20 | // TODO: Replace productId with automatic fetch from Vertex contracts through manager.
21 | constructor(
22 | address _base,
23 | address _manager,
24 | address _externalAccount,
25 | uint32 _productId,
26 | address _token,
27 | bool _spot
28 | ) {
29 | base = IERC20Metadata(_base);
30 | manager = VertexManager(_manager);
31 | externalAccount = _externalAccount;
32 | productId = _productId;
33 | token = _token;
34 | spot = _spot;
35 | }
36 |
37 | function setup() internal {
38 | // Get the token decimals.
39 | uint256 decimals = IERC20Metadata(token).decimals();
40 |
41 | // Start broadcast.
42 | vm.startBroadcast();
43 |
44 | // Add token support.
45 | manager.updateToken(token, productId);
46 |
47 | // Check if allowance is needed.
48 | if (base.allowance(msg.sender, address(manager)) < type(uint256).max) {
49 | base.approve(address(manager), type(uint256).max);
50 | }
51 |
52 | if (spot) {
53 | tokens = new address[](2);
54 | tokens[0] = token;
55 | tokens[1] = address(base);
56 |
57 | hardcaps = new uint256[](2);
58 | hardcaps[0] = 0;
59 | hardcaps[1] = 0;
60 |
61 | manager.addPool(productId, tokens, hardcaps, IVertexManager.PoolType.Spot, externalAccount);
62 | } else {
63 | tokens = new address[](1);
64 | tokens[0] = address(base);
65 |
66 | hardcaps = new uint256[](1);
67 | hardcaps[0] = 0;
68 |
69 | manager.addPool(productId, tokens, hardcaps, IVertexManager.PoolType.Perp, externalAccount);
70 | }
71 |
72 | vm.stopBroadcast();
73 | }
74 |
75 | // Exclude from coverage report
76 | function test() public virtual {}
77 | }
78 |
--------------------------------------------------------------------------------
/script/pools/vrtx/AddMainnet.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Script.sol";
5 | import {AddPool} from "script/pools/AddBase.s.sol";
6 |
7 | contract AddVertex is AddPool {
8 | constructor()
9 | AddPool(
10 | 0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8,
11 | 0x052Ab3fd33cADF9D9f227254252da3f996431f75,
12 | 0xD7cb7F791bb97A1a8B5aFc3aec5fBD0BEC4536A5,
13 | 41,
14 | 0x95146881b86B3ee99e63705eC87AfE29Fcc044D9,
15 | true
16 | )
17 | {}
18 |
19 | function run() external {
20 | setup();
21 | }
22 |
23 | // Exclude from coverage report
24 | function test() public override {}
25 | }
26 |
--------------------------------------------------------------------------------
/script/pools/vrtx/AddSepolia.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Script.sol";
5 | import {AddPool} from "script/pools/AddBase.s.sol";
6 |
7 | contract AddVertex is AddPool {
8 | constructor()
9 | AddPool(
10 | 0xbC47901f4d2C5fc871ae0037Ea05c3F614690781,
11 | 0x052Ab3fd33cADF9D9f227254252da3f996431f75,
12 | 0x28CcdB531854d09D48733261688dc1679fb9A242,
13 | 41,
14 | 0x00aBCa5597d51e6C06eCfA655E73CE70A1e2cdCf,
15 | true
16 | )
17 | {}
18 |
19 | function run() external {
20 | setup();
21 | }
22 |
23 | // Exclude from coverage report
24 | function test() public override {}
25 | }
26 |
--------------------------------------------------------------------------------
/slither.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "filter_paths": "(script/|test/|lib/)",
3 | "solc_remaps": "forge-std/=lib/forge-std/src/ ds-test/=lib/ds-test/src/ openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ openzeppelin/=lib/openzeppelin-contracts/contracts/"
4 | }
5 |
--------------------------------------------------------------------------------
/src/Distributor.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import {ECDSA} from "openzeppelin/utils/cryptography/ECDSA.sol";
5 | import {EIP712} from "openzeppelin/utils/cryptography/EIP712.sol";
6 | import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
7 | import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
8 | import {Ownable} from "openzeppelin/access/Ownable.sol";
9 |
10 | /// @title Elixir distributor
11 | /// @author The Elixir Team
12 | /// @custom:security-contact security@elixir.finance
13 | /// @notice Allows users to claim a token amount, approved by Elixir.
14 | contract Distributor is Ownable, EIP712 {
15 | using SafeERC20 for IERC20;
16 |
17 | /*//////////////////////////////////////////////////////////////
18 | VARIABLES
19 | //////////////////////////////////////////////////////////////*/
20 |
21 | /// @notice Track claimed actions.
22 | mapping(bytes32 => bool) public claimed;
23 |
24 | /// @notice The Elixir signer address.
25 | address public immutable signer;
26 |
27 | /*//////////////////////////////////////////////////////////////
28 | EVENTS
29 | //////////////////////////////////////////////////////////////*/
30 |
31 | /// @notice Emitted when a user claims a token amount.
32 | /// @param caller The caller of the transaction.
33 | /// @param receiver The receiver of the rewards.
34 | /// @param token The token claimed.
35 | /// @param amount The amount of rewards claimed.
36 | /// @param nonce The nonce of the action.
37 | event Claimed(
38 | address caller, address indexed receiver, address indexed token, uint256 indexed amount, uint256 nonce
39 | );
40 |
41 | /// @notice Emitted when the owner withdraws a token.
42 | /// @param token The token withdrawn.
43 | /// @param amount The amount of token withdrawn.
44 | event Withdraw(address indexed token, uint256 indexed amount);
45 |
46 | /*//////////////////////////////////////////////////////////////
47 | ERRORS
48 | //////////////////////////////////////////////////////////////*/
49 |
50 | /// @notice Error emitted when the ECDSA signer is not correct.
51 | error InvalidSignature();
52 |
53 | /// @notice Error emitted when the user has already claimed.
54 | error AlreadyClaimed();
55 |
56 | /// @notice Error emitted when the amount is zero.
57 | error InvalidAmount();
58 |
59 | /// @notice Error emitted when the nonce is zero.
60 | error InvalidNonce();
61 |
62 | /// @notice Error emitted when the token is zero.
63 | error InvalidToken();
64 |
65 | /*//////////////////////////////////////////////////////////////
66 | CONSTRUCTOR
67 | //////////////////////////////////////////////////////////////*/
68 |
69 | /// @notice Set the storage variables.
70 | /// @param _name The name of the contract.
71 | /// @param _version The version of the contract.
72 | /// @param _signer The Elixir signer address.
73 | constructor(string memory _name, string memory _version, address _signer) EIP712(_name, _version) {
74 | // Set the signer address.
75 | signer = _signer;
76 | }
77 |
78 | /*//////////////////////////////////////////////////////////////
79 | EXTERNAL FUNCTIONS
80 | //////////////////////////////////////////////////////////////*/
81 |
82 | /// @notice Claims tokens approved by Elixir.
83 | /// @param receiver The receiver of the claim.
84 | /// @param token The token to claim.
85 | /// @param amount The amount of token to claim.
86 | /// @param nonce The nonce of the action.
87 | /// @param signature The signature from the Elixir signer.
88 | function claim(address receiver, address token, uint256 amount, uint256 nonce, bytes memory signature) external {
89 | // Check that the token is not zero.
90 | if (token == address(0)) revert InvalidToken();
91 |
92 | // Check that the amount is not zero.
93 | if (amount == 0) revert InvalidAmount();
94 |
95 | // Check that the nonce is not zero.
96 | if (nonce == 0) revert InvalidNonce();
97 |
98 | // Generate digest.
99 | bytes32 digest = _hashTypedDataV4(
100 | keccak256(
101 | abi.encode(
102 | keccak256("Claim(address user,address token,uint256 amount,uint256 nonce)"),
103 | receiver,
104 | token,
105 | amount,
106 | nonce
107 | )
108 | )
109 | );
110 |
111 | // Check if the user already claimed.
112 | if (claimed[digest]) revert AlreadyClaimed();
113 |
114 | // Check if the signature is valid.
115 | if (ECDSA.recover(digest, signature) != signer) revert InvalidSignature();
116 |
117 | // Mark nonce as claimed.
118 | claimed[digest] = true;
119 |
120 | // Transfer tokens to user.
121 | IERC20(token).safeTransfer(receiver, amount);
122 |
123 | emit Claimed(msg.sender, receiver, token, amount, nonce);
124 | }
125 |
126 | /// @notice Withdraw a given amount of tokens.
127 | function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
128 | IERC20(token).safeTransfer(owner(), amount);
129 |
130 | emit Withdraw(token, amount);
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/VertexManager.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
5 | import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
6 | import {Math} from "openzeppelin/utils/math/Math.sol";
7 | import {ReentrancyGuard} from "openzeppelin/security/ReentrancyGuard.sol";
8 |
9 | import {Initializable} from "openzeppelin-upgradeable/proxy/utils/Initializable.sol";
10 | import {UUPSUpgradeable} from "openzeppelin-upgradeable/proxy/utils/UUPSUpgradeable.sol";
11 | import {OwnableUpgradeable} from "openzeppelin-upgradeable/access/OwnableUpgradeable.sol";
12 |
13 | import {IVertexManager} from "src/interfaces/IVertexManager.sol";
14 | import {IEndpoint} from "src/interfaces/IEndpoint.sol";
15 | import {IClearinghouse} from "src/interfaces/IClearinghouse.sol";
16 |
17 | import {VertexProcessor} from "src/VertexProcessor.sol";
18 | import {VertexStorage} from "src/VertexStorage.sol";
19 | import {VertexRouter} from "src/VertexRouter.sol";
20 |
21 | /// @title Elixir pool manager for Vertex
22 | /// @author The Elixir Team
23 | /// @custom:security-contact security@elixir.finance
24 | /// @notice Pool manager contract to provide liquidity for spot and perp market making on Vertex Protocol.
25 | contract VertexManager is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard, VertexStorage {
26 | using Math for uint256;
27 | using SafeERC20 for IERC20Metadata;
28 |
29 | /*//////////////////////////////////////////////////////////////
30 | EVENTS
31 | //////////////////////////////////////////////////////////////*/
32 |
33 | /// @notice Emitted when a perp withdrawal is queued.
34 | /// @param spot The spot added to the queue.
35 | /// @param queueCount The queue count.
36 | /// @param queueUpTo The queue up to.
37 | event Queued(Spot spot, uint128 queueCount, uint128 queueUpTo);
38 |
39 | /// @notice Emitted when a claim is made.
40 | /// @param user The user for which the tokens were claimed.
41 | /// @param token The token claimed.
42 | /// @param amount The token amount claimed.
43 | event Claim(address indexed user, address indexed token, uint256 indexed amount);
44 |
45 | /// @notice Emitted when the pause statuses are updated.
46 | /// @param depositPaused True if deposits are paused, false otherwise.
47 | /// @param withdrawPaused True if withdrawals are paused, false otherwise.
48 | /// @param claimPaused True if claims are paused, false otherwise.
49 | event PauseUpdated(bool indexed depositPaused, bool indexed withdrawPaused, bool indexed claimPaused);
50 |
51 | /// @notice Emitted when a pool is added.
52 | /// @param id The ID of the pool.
53 | /// @param poolType The type of the pool.
54 | /// @param router The router address of the pool.
55 | /// @param tokens The tokens of the pool.
56 | /// @param hardcaps The hardcaps of the pool.
57 | event PoolAdded(
58 | uint256 indexed id, PoolType poolType, address indexed router, address[] tokens, uint256[] hardcaps
59 | );
60 |
61 | /// @notice Emitted when tokens are added to a pool.
62 | /// @param id The ID of the pool.
63 | /// @param tokens The new tokens of the pool.
64 | /// @param hardcaps The hardcaps of the added tokens.
65 | event PoolTokensAdded(uint256 indexed id, address[] tokens, uint256[] hardcaps);
66 |
67 | /// @notice Emitted when a pool's hardcaps are updated.
68 | /// @param id The ID of the pool.
69 | /// @param hardcaps The new hardcaps of the pool.
70 | event PoolHardcapsUpdated(uint256 indexed id, uint256[] hardcaps);
71 |
72 | /// @notice Emitted when the Vertex product ID of a token is updated.
73 | /// @param token The token address.
74 | /// @param productId The new Vertex product ID of the token.
75 | event TokenUpdated(address indexed token, uint256 indexed productId);
76 |
77 | /// @notice Emitted when the queue gets updated (when a spot is queued or unqueued).
78 | /// @param queueCount The new queue count.
79 | /// @param queueUpTo The new queue up to.
80 | /// @param nextSpot The next spot in the queue.
81 | /// @param eventType The type of queue update.
82 | event QueueUpdated(uint128 queueCount, uint128 queueUpTo, Spot nextSpot, QueueEvent eventType);
83 |
84 | /*//////////////////////////////////////////////////////////////
85 | ERRORS
86 | //////////////////////////////////////////////////////////////*/
87 |
88 | /// @notice Emitted when the receiver is the zero address.
89 | error ZeroAddress();
90 |
91 | /// @notice Emitted when a token is duplicated.
92 | /// @param token The duplicated token.
93 | error DuplicatedToken(address token);
94 |
95 | /// @notice Emitted when deposits are paused.
96 | error DepositsPaused();
97 |
98 | /// @notice Emitted when withdrawals are paused.
99 | error WithdrawalsPaused();
100 |
101 | /// @notice Emitted when claims are paused.
102 | error ClaimsPaused();
103 |
104 | /// @notice Emitted when the length of two arrays don't match.
105 | /// @param array1 The uint256 array input.
106 | /// @param array2 The address array input.
107 | error MismatchInputs(uint256[] array1, address[] array2);
108 |
109 | /// @notice Emitted when the pool is not valid or used in the incorrect function.
110 | /// @param id The ID of the pool.
111 | error InvalidPool(uint256 id);
112 |
113 | /// @notice Emitted when the token is not valid because it has more than 18 decimals.
114 | /// @param token The address of the token.
115 | error InvalidToken(address token);
116 |
117 | /// @notice Emitted when the amount given to withdraw is less than the fee to pay.
118 | /// @param amount The amount given to withdraw.
119 | /// @param fee The fee to pay.
120 | error AmountTooLow(uint256 amount, uint256 fee);
121 |
122 | /// @notice Emitted when the given spot ID to unqueue is not valid.
123 | error InvalidSpot(uint128 spotId, uint128 queueUpTo);
124 |
125 | /// @notice Emitted when the caller is not the external account of the pool's router.
126 | error NotExternalAccount(address router, address externalAccount, address caller);
127 |
128 | /// @notice Emitted when the msg.value of the call is too low for the fee.
129 | /// @param value The msg.value.
130 | /// @param fee The fee to pay.
131 | error FeeTooLow(uint256 value, uint256 fee);
132 |
133 | /// @notice Emitted when the fee transfer fails.
134 | error FeeTransferFailed();
135 |
136 | /// @notice Emitted when the price returned is zero.
137 | error ZeroPrice();
138 |
139 | /*//////////////////////////////////////////////////////////////
140 | MODIFIERS
141 | //////////////////////////////////////////////////////////////*/
142 |
143 | /// @notice Reverts when deposits are paused.
144 | modifier whenDepositNotPaused() {
145 | if (depositPaused) revert DepositsPaused();
146 | _;
147 | }
148 |
149 | /// @notice Reverts when withdrawals are paused.
150 | modifier whenWithdrawNotPaused() {
151 | if (withdrawPaused) revert WithdrawalsPaused();
152 | _;
153 | }
154 |
155 | /// @notice Reverts when claims are paused.
156 | modifier whenClaimNotPaused() {
157 | if (claimPaused) revert ClaimsPaused();
158 | _;
159 | }
160 |
161 | /*//////////////////////////////////////////////////////////////
162 | CONSTRUCTOR
163 | //////////////////////////////////////////////////////////////*/
164 |
165 | /// @notice Prevent the implementation contract from being initialized.
166 | /// @dev The proxy contract state will still be able to call this function because the constructor does not affect the proxy state.
167 | constructor() {
168 | _disableInitializers();
169 | }
170 |
171 | /*//////////////////////////////////////////////////////////////
172 | INITIALIZER
173 | //////////////////////////////////////////////////////////////*/
174 |
175 | /// @notice No constructor in upgradable contracts, so initialized with this function.
176 | /// @param _endpoint The address of the Vertex Endpoint contract.
177 | /// @param _processor The address of the VertexProcessor contract.
178 | /// @param _slowModeFee The fee to pay Vertex for slow mode transactions.
179 | function initialize(address _endpoint, address _processor, uint256 _slowModeFee) public initializer {
180 | __UUPSUpgradeable_init();
181 | __Ownable_init();
182 |
183 | // Set Vertex's endpoint address.
184 | endpoint = IEndpoint(_endpoint);
185 |
186 | // Set the processor address.
187 | processor = _processor;
188 |
189 | // Set the slow mode fee.
190 | slowModeFee = _slowModeFee;
191 |
192 | // Set the quote token for slow-mode transactions through Vertex.
193 | quoteToken = IERC20Metadata(IClearinghouse(endpoint.clearinghouse()).getQuote());
194 | }
195 |
196 | /*//////////////////////////////////////////////////////////////
197 | DEPOSIT/WITHDRAWAL ENTRY
198 | //////////////////////////////////////////////////////////////*/
199 |
200 | /// @notice Deposit a token into a perp pool.
201 | /// @param id The pool ID.
202 | /// @param token The token to deposit.
203 | /// @param amount The token amount to deposit.
204 | /// @param receiver The receiver of the virtual LP balance.
205 | function depositPerp(uint256 id, address token, uint256 amount, address receiver)
206 | external
207 | payable
208 | whenDepositNotPaused
209 | nonReentrant
210 | {
211 | // Fetch the pool storage.
212 | Pool storage pool = pools[id];
213 |
214 | // Check that the pool is perp.
215 | if (pool.poolType != PoolType.Perp) revert InvalidPool(id);
216 |
217 | // Check that the receiver is not the zero address.
218 | if (receiver == address(0)) revert ZeroAddress();
219 |
220 | // Take fee for unqueue transaction.
221 | takeElixirFee(pool.router);
222 |
223 | // Add to queue.
224 | queue[queueCount++] = Spot(
225 | msg.sender,
226 | pool.router,
227 | SpotType.DepositPerp,
228 | abi.encode(DepositPerp({id: id, token: token, amount: amount, receiver: receiver}))
229 | );
230 |
231 | emit Queued(queue[queueCount - 1], queueCount, queueUpTo);
232 | emit QueueUpdated(queueCount, queueUpTo, queue[queueUpTo], QueueEvent.Deposit);
233 | }
234 |
235 | /// @notice Deposits tokens into a spot pool.
236 | /// @dev Requests are placed into a FIFO queue, which is processed by the Elixir market-making network and passed on to Vertex via the `unqueue` function.
237 | /// @param id The ID of the pool to deposit.
238 | /// @param token0 The base token.
239 | /// @param token1 The quote token.
240 | /// @param amount0 The amount of base tokens.
241 | /// @param amount1Low The low limit of the quote token amount.
242 | /// @param amount1High The high limit of the quote token amount.
243 | /// @param receiver The receiver of the virtual LP balance.
244 | function depositSpot(
245 | uint256 id,
246 | address token0,
247 | address token1,
248 | uint256 amount0,
249 | uint256 amount1Low,
250 | uint256 amount1High,
251 | address receiver
252 | ) external payable whenDepositNotPaused nonReentrant {
253 | // Fetch the pool storage.
254 | Pool storage pool = pools[id];
255 |
256 | // Check that the pool is spot.
257 | if (pool.poolType != PoolType.Spot) revert InvalidPool(id);
258 |
259 | // Check that the tokens are not duplicated.
260 | if (token0 == token1) revert DuplicatedToken(token0);
261 |
262 | // Check that the receiver is not the zero address.
263 | if (receiver == address(0)) revert ZeroAddress();
264 |
265 | // Take fee for unqueue transaction.
266 | takeElixirFee(pool.router);
267 |
268 | // Add to queue.
269 | queue[queueCount++] = Spot(
270 | msg.sender,
271 | pool.router,
272 | SpotType.DepositSpot,
273 | abi.encode(
274 | DepositSpot({
275 | id: id,
276 | token0: token0,
277 | token1: token1,
278 | amount0: amount0,
279 | amount1Low: amount1Low,
280 | amount1High: amount1High,
281 | receiver: receiver
282 | })
283 | )
284 | );
285 |
286 | emit Queued(queue[queueCount - 1], queueCount, queueUpTo);
287 | emit QueueUpdated(queueCount, queueUpTo, queue[queueUpTo], QueueEvent.Deposit);
288 | }
289 |
290 | /// @notice Requests to withdraw a token from a perp pool.
291 | /// @dev Requests are placed into a FIFO queue, which is processed by the Elixir market-making network and passed on to Vertex via the `unqueue` function.
292 | /// @dev After processed by Vertex, the user (or anyone on behalf of it) can call the `claim` function.
293 | /// @param id The ID of the pool to withdraw from.
294 | /// @param token The token to withdraw.
295 | /// @param amount The amount of token shares to withdraw.
296 | function withdrawPerp(uint256 id, address token, uint256 amount)
297 | external
298 | payable
299 | whenWithdrawNotPaused
300 | nonReentrant
301 | {
302 | // Fetch the pool storage.
303 | Pool storage pool = pools[id];
304 |
305 | // Check that the pool is perp.
306 | if (pool.poolType != PoolType.Perp) revert InvalidPool(id);
307 |
308 | // Check that the amount is at least the Vertex fee to pay.
309 | uint256 fee = getTransactionFee(token);
310 |
311 | if (amount < fee) revert AmountTooLow(amount, fee);
312 |
313 | // Take fee for unqueue transaction.
314 | takeElixirFee(pool.router);
315 |
316 | // Add to queue.
317 | queue[queueCount++] = Spot(
318 | msg.sender,
319 | pool.router,
320 | SpotType.WithdrawPerp,
321 | abi.encode(WithdrawPerp({id: id, token: token, amount: amount}))
322 | );
323 |
324 | emit Queued(queue[queueCount - 1], queueCount, queueUpTo);
325 | emit QueueUpdated(queueCount, queueUpTo, queue[queueUpTo], QueueEvent.Withdraw);
326 | }
327 |
328 | /// @notice Withdraws tokens from a spot pool.
329 | /// @dev Requests are placed into a FIFO queue, which is processed by the Elixir market-making network and passed on to Vertex via the `unqueue` function.
330 | /// @param id The ID of the pool to withdraw from.
331 | /// @param token0 The base token.
332 | /// @param token1 The quote token.
333 | /// @param amount0 The amount of base tokens.
334 | function withdrawSpot(uint256 id, address token0, address token1, uint256 amount0)
335 | external
336 | payable
337 | whenWithdrawNotPaused
338 | nonReentrant
339 | {
340 | // Fetch the pool data.
341 | Pool storage pool = pools[id];
342 |
343 | // Check that the pool is spot.
344 | if (pool.poolType != PoolType.Spot) revert InvalidPool(id);
345 |
346 | // Check that the tokens are not duplicated.
347 | if (token0 == token1) revert DuplicatedToken(token0);
348 |
349 | // Take fee for unqueue transaction.
350 | takeElixirFee(pool.router);
351 |
352 | // Add to queue.
353 | queue[queueCount++] = Spot(
354 | msg.sender,
355 | pool.router,
356 | SpotType.WithdrawSpot,
357 | abi.encode(WithdrawSpot({id: id, token0: token0, token1: token1, amount0: amount0}))
358 | );
359 |
360 | emit Queued(queue[queueCount - 1], queueCount, queueUpTo);
361 | emit QueueUpdated(queueCount, queueUpTo, queue[queueUpTo], QueueEvent.Withdraw);
362 | }
363 |
364 | /// @notice Claim received tokens from the pending balance and fees.
365 | /// @param user The address to claim for.
366 | /// @param token The token to claim.
367 | /// @param id The ID of the pool to claim from.
368 | function claim(address user, address token, uint256 id) external whenClaimNotPaused nonReentrant {
369 | // Fetch the pool data.
370 | Pool storage pool = pools[id];
371 |
372 | // Check that the pool exists.
373 | if (pool.router == address(0)) revert InvalidPool(id);
374 |
375 | // Check that the user is not the zero address.
376 | if (user == address(0)) revert ZeroAddress();
377 |
378 | // Fetch the pool router.
379 | VertexRouter router = VertexRouter(pool.router);
380 |
381 | // Establish empty token data.
382 | Token storage tokenData;
383 |
384 | // If token is the Clearinghouse quote token, point to the old quote token data.
385 | if (oldQuoteToken != address(0) && token == address(quoteToken)) {
386 | tokenData = pool.tokens[oldQuoteToken];
387 | } else {
388 | tokenData = pool.tokens[token];
389 | }
390 |
391 | // Get Elixir's pending fee balance.
392 | uint256 fee = tokenData.fees[user];
393 |
394 | // Calculate the user's claim amount.
395 | uint256 claim =
396 | Math.min(tokenData.userPendingAmount[user] + fee, IERC20Metadata(token).balanceOf(address(router)));
397 |
398 | // Resets the pending balance of the user.
399 | tokenData.userPendingAmount[user] -= claim - fee;
400 |
401 | // Resets the Elixir pending fee balance.
402 | tokenData.fees[user] -= fee;
403 |
404 | // Fetch the tokens from the router.
405 | router.claimToken(token, claim);
406 |
407 | // Transfers the tokens after to prevent reentrancy.
408 | IERC20Metadata(token).safeTransfer(owner(), fee);
409 | IERC20Metadata(token).safeTransfer(user, claim - fee);
410 |
411 | emit Claim(user, token, claim - fee);
412 | }
413 |
414 | /*//////////////////////////////////////////////////////////////
415 | HELPERS
416 | //////////////////////////////////////////////////////////////*/
417 |
418 | /// @notice Returns the price on Vertex of a given by product.
419 | /// @param id The ID of the product to get the price of.
420 | function getPrice(uint32 id) public view returns (uint256 price) {
421 | // If id is 0 (quote), return default price.
422 | price = (id == 0) ? 1e18 : endpoint.getPriceX18(id);
423 |
424 | if (price == 0) revert ZeroPrice();
425 | }
426 |
427 | /// @notice Returns the data a pool and a token within it.
428 | /// @param id The ID of the pool supporting the token.
429 | /// @param token The token to fetch the data of.
430 | function getPoolToken(uint256 id, address token) external view returns (address, uint256, uint256, bool) {
431 | // Get the pool data.
432 | Pool storage pool = pools[id];
433 |
434 | // Establish empty token data.
435 | Token storage tokenData;
436 |
437 | // If token is the Clearinghouse quote token, point to the old quote token data.
438 | if (oldQuoteToken != address(0) && token == address(quoteToken)) {
439 | tokenData = pool.tokens[oldQuoteToken];
440 | } else {
441 | tokenData = pool.tokens[token];
442 | }
443 |
444 | return (pool.router, tokenData.activeAmount, tokenData.hardcap, tokenData.isActive);
445 | }
446 |
447 | /// @notice Returns the slow-mode fee for a given pool and token.
448 | /// @param token The token to fetch the fee from.
449 | function getTransactionFee(address token) public view returns (uint256) {
450 | return slowModeFee.mulDiv(
451 | 10 ** (18 + IERC20Metadata(token).decimals() - quoteToken.decimals()),
452 | getPrice(tokenToProduct[token]),
453 | Math.Rounding.Up
454 | );
455 | }
456 |
457 | /// @notice Returns a user's active amount for a token within a pool.
458 | /// @param id The ID of the pool to fetch the active amounts of.
459 | /// @param token The token to fetch the active amounts of.
460 | /// @param user The user to fetch the active amounts of.
461 | function getUserActiveAmount(uint256 id, address token, address user) external view returns (uint256) {
462 | // If token is the Clearinghouse quote token, point to the old quote token.
463 | if (oldQuoteToken != address(0) && token == address(quoteToken)) {
464 | token = oldQuoteToken;
465 | }
466 |
467 | return pools[id].tokens[token].userActiveAmount[user];
468 | }
469 |
470 | /// @notice Returns a user's pending amount for a token within a pool.
471 | /// @param id The ID of the pool to fetch the pending amount of.
472 | /// @param token The token to fetch the pending amount of.
473 | /// @param user The user to fetch the pending amount of.
474 | function getUserPendingAmount(uint256 id, address token, address user) external view returns (uint256) {
475 | // If token is the Clearinghouse quote token, point to the old quote token.
476 | if (oldQuoteToken != address(0) && token == address(quoteToken)) {
477 | token = oldQuoteToken;
478 | }
479 |
480 | return pools[id].tokens[token].userPendingAmount[user];
481 | }
482 |
483 | /// @notice Returns a user's reimbursement fee for a token within a pool.
484 | /// @param id The ID of the pool to fetch the fee for.
485 | /// @param token The token to fetch the fee for.
486 | /// @param user The user to fetch the fee for.
487 | function getUserFee(uint256 id, address token, address user) external view returns (uint256) {
488 | // If token is the Clearinghouse quote token, point to the old quote token.
489 | if (oldQuoteToken != address(0) && token == address(quoteToken)) {
490 | token = oldQuoteToken;
491 | }
492 |
493 | return pools[id].tokens[token].fees[user];
494 | }
495 |
496 | /// @notice Returns the balanced amount of quote tokens given an amount of base tokens.
497 | /// @param token0 The base token.
498 | /// @param token1 The quote token.
499 | /// @param amount0 The amount of base tokens.
500 | function getBalancedAmount(address token0, address token1, uint256 amount0) external view returns (uint256) {
501 | return amount0.mulDiv(
502 | (getPrice(tokenToProduct[token0]) * (10 ** 18)) / getPrice(tokenToProduct[token1]),
503 | 10 ** (18 + IERC20Metadata(token0).decimals() - IERC20Metadata(token1).decimals()),
504 | Math.Rounding.Down
505 | );
506 | }
507 |
508 | /// @notice Returns the calculated amount of tokens to receive when withdrawing, given an amount of tokens and the pool balance on Vertex.
509 | /// @param balance The Vertex balance of the pool.
510 | /// @param amount The amount of tokens withdrawing.
511 | /// @param activeAmount The active amount of tokens in the pool.
512 | function getWithdrawAmount(uint256 balance, uint256 amount, uint256 activeAmount) public pure returns (uint256) {
513 | // Calculate the amount to receive via percentage of ownership, accounting for any trading losses.
514 | return amount.mulDiv(balance, activeAmount, Math.Rounding.Down);
515 | }
516 |
517 | /// @notice Returns the external account of a pool router.
518 | function getExternalAccount(address router) private view returns (address) {
519 | return routerSigner[router];
520 | }
521 |
522 | /// @notice Enforce the Elixir fee in native ETH.
523 | /// @param router The pool router.
524 | function takeElixirFee(address router) private {
525 | // Get the Elixir processing fee for unqueue transaction using WETH as token.
526 | // Safely assumes that WETH ID on Vertex is 3.
527 | uint256 fee = getTransactionFee(productToToken[3]);
528 |
529 | // Check that the msg.value is equal or more than the fee.
530 | if (msg.value < fee) revert FeeTooLow(msg.value, fee);
531 |
532 | // Transfer fee to the external account EOA.
533 | (bool sent,) = payable(getExternalAccount(router)).call{value: msg.value}("");
534 | if (!sent) revert FeeTransferFailed();
535 | }
536 |
537 | /// @notice Returns the next spot in the queue to process.
538 | function nextSpot() external view returns (Spot memory) {
539 | return queue[queueUpTo];
540 | }
541 |
542 | /*//////////////////////////////////////////////////////////////
543 | PERMISSIONED FUNCTIONS
544 | //////////////////////////////////////////////////////////////*/
545 |
546 | /// @notice Processes the next spot in the withdraw perp queue.
547 | /// @param spotId The ID of the spot queue to process.
548 | /// @param response The response to the spot transaction.
549 | function unqueue(uint128 spotId, bytes memory response) external {
550 | // Get the spot data from the queue.
551 | Spot memory spot = queue[queueUpTo];
552 |
553 | // Get the external account of the router.
554 | address externalAccount = getExternalAccount(spot.router);
555 |
556 | // Check that the sender is the external account of the router.
557 | if (msg.sender != externalAccount) {
558 | revert NotExternalAccount(spot.router, externalAccount, msg.sender);
559 | }
560 |
561 | if (response.length != 0) {
562 | // Check that next spot in queue matches the given spot ID.
563 | if (spotId != queueUpTo + 1) revert InvalidSpot(spotId, queueUpTo);
564 |
565 | // Process spot. Skips if fail or revert.
566 | bytes memory processorCall =
567 | abi.encodeWithSelector(VertexProcessor.processSpot.selector, spot, response, address(this));
568 | processor.delegatecall(processorCall);
569 | } else {
570 | // Intetionally skip.
571 | }
572 |
573 | // Increase the queue up to.
574 | queueUpTo++;
575 |
576 | emit QueueUpdated(queueCount, queueUpTo, queue[queueUpTo], QueueEvent.Unqueue);
577 | }
578 |
579 | /// @notice Manages the paused status of deposits, withdrawals, and claims
580 | /// @param _depositPaused True to pause deposits, false otherwise.
581 | /// @param _withdrawPaused True to pause withdrawals, false otherwise.
582 | /// @param _claimPaused True to pause claims, false otherwise.
583 | function pause(bool _depositPaused, bool _withdrawPaused, bool _claimPaused) external onlyOwner {
584 | depositPaused = _depositPaused;
585 | withdrawPaused = _withdrawPaused;
586 | claimPaused = _claimPaused;
587 |
588 | emit PauseUpdated(depositPaused, withdrawPaused, claimPaused);
589 | }
590 |
591 | /// @notice Adds a new pool.
592 | /// @param id The ID of the new pool.
593 | /// @param tokens The tokens to add.
594 | /// @param hardcaps The hardcaps for the tokens.
595 | /// @param poolType The type of the pool.
596 | /// @param externalAccount The external account to link to the Vertex Endpoint.
597 | function addPool(
598 | uint256 id,
599 | address[] calldata tokens,
600 | uint256[] calldata hardcaps,
601 | PoolType poolType,
602 | address externalAccount
603 | ) external onlyOwner {
604 | // Check that the pool doesn't exist.
605 | if (pools[id].router != address(0)) revert InvalidPool(id);
606 |
607 | // Deploy a new router contract.
608 | VertexRouter router = new VertexRouter(address(endpoint), externalAccount);
609 |
610 | // Approve the fee token to the router.
611 | router.makeApproval(address(quoteToken));
612 |
613 | // Create LinkSigner request for Vertex.
614 | IEndpoint.LinkSigner memory linkSigner =
615 | IEndpoint.LinkSigner(router.contractSubaccount(), router.externalSubaccount(), 0);
616 |
617 | // Fetch payment fee from owner. This can be reimbursed on withdrawals after tokens are received.
618 | quoteToken.safeTransferFrom(owner(), address(router), slowModeFee);
619 |
620 | // Submit slow-mode tx to Vertex.
621 | router.submitSlowModeTransaction(
622 | abi.encodePacked(uint8(IEndpoint.TransactionType.LinkSigner), abi.encode(linkSigner))
623 | );
624 |
625 | // invariant: poolId always has same router
626 | // Adds signer (external account) to the signer mapping
627 | routerSigner[address(router)] = externalAccount;
628 |
629 | // Set the router address of the pool.
630 | pools[id].router = address(router);
631 |
632 | // Set the pool type.
633 | pools[id].poolType = poolType;
634 |
635 | // Add tokens to pool.
636 | addPoolTokens(id, tokens, hardcaps);
637 |
638 | emit PoolAdded(id, poolType, address(router), tokens, hardcaps);
639 | }
640 |
641 | /// @notice Updates linked signers for given pools
642 | /// @param ids The IDs of the pools.
643 | /// @param signers The new signers to link
644 | function updateLinkedSigners(uint256[] calldata ids, address[] calldata signers) external onlyOwner {
645 | if (ids.length != signers.length) {
646 | revert MismatchInputs(ids, signers);
647 | }
648 |
649 | for (uint256 i = 0; i < ids.length; i++) {
650 | VertexRouter router = VertexRouter(pools[ids[i]].router);
651 |
652 | bytes32 newSigner = bytes32(uint256(uint160(signers[i])) << 96);
653 |
654 | // Create LinkSigner request for Vertex.
655 | IEndpoint.LinkSigner memory linkSigner = IEndpoint.LinkSigner(router.contractSubaccount(), newSigner, 0);
656 |
657 | // Fetch payment fee from owner for transaction.
658 | quoteToken.safeTransferFrom(owner(), address(router), slowModeFee);
659 |
660 | // Submit slow-mode tx to Vertex to update signer
661 | router.submitSlowModeTransaction(
662 | abi.encodePacked(uint8(IEndpoint.TransactionType.LinkSigner), abi.encode(linkSigner))
663 | );
664 |
665 | // Update signer in our mapping
666 | routerSigner[address(router)] = signers[i];
667 | }
668 | }
669 |
670 | /// @notice Adds new tokens to a pool.
671 | /// @param id The ID of the pool.
672 | /// @param tokens The tokens to add.
673 | /// @param hardcaps The hardcaps for the tokens.
674 | function addPoolTokens(uint256 id, address[] calldata tokens, uint256[] calldata hardcaps) public onlyOwner {
675 | // Fetch the pool router.
676 | VertexRouter router = VertexRouter(pools[id].router);
677 |
678 | // Loop over tokens to add.
679 | for (uint256 i = 0; i < tokens.length; i++) {
680 | // Get the token address.
681 | address token = tokens[i];
682 |
683 | // Check that the token decimals are below or equal to 18 decimals (Vertex maximum).
684 | if (IERC20Metadata(token).decimals() > 18) {
685 | revert InvalidToken(token);
686 | }
687 |
688 | // Fetch the token data storage within the pool.
689 | Token storage tokenData;
690 |
691 | // If token is the Clearinghouse quote token, point to the old quote token data.
692 | if (oldQuoteToken != address(0) && token == address(quoteToken)) {
693 | tokenData = pools[id].tokens[oldQuoteToken];
694 | } else {
695 | tokenData = pools[id].tokens[token];
696 | }
697 |
698 | // Enable if the token is not already supported,.
699 | if (!tokenData.isActive) tokenData.isActive = true;
700 |
701 | // Add the hardcap to the token data.
702 | tokenData.hardcap = hardcaps[i];
703 |
704 | // Make router approve tokens to Vertex endpoint.
705 | router.makeApproval(token);
706 | }
707 |
708 | emit PoolTokensAdded(id, tokens, hardcaps);
709 | }
710 |
711 | /// @notice Updates the hardcaps of a pool.
712 | /// @param id The ID of the pool.
713 | /// @param tokens The list of tokens to update the hardcaps of.
714 | /// @param hardcaps The hardcaps for the tokens.
715 | function updatePoolHardcaps(uint256 id, address[] calldata tokens, uint256[] calldata hardcaps)
716 | external
717 | onlyOwner
718 | {
719 | // Check that the length of the hardcaps array matches the pool tokens length.
720 | if (hardcaps.length != tokens.length) {
721 | revert MismatchInputs(hardcaps, tokens);
722 | }
723 |
724 | // Loop over hardcaps to update.
725 | for (uint256 i = 0; i < hardcaps.length; i++) {
726 | // Get the token.
727 | address token = tokens[i];
728 |
729 | // If token is the Clearinghouse quote token, point to the old quote token.
730 | if (oldQuoteToken != address(0) && token == address(quoteToken)) {
731 | token = oldQuoteToken;
732 | }
733 |
734 | pools[id].tokens[token].hardcap = hardcaps[i];
735 | }
736 |
737 | emit PoolHardcapsUpdated(id, hardcaps);
738 | }
739 |
740 | /// @notice Updates the Vertex product ID of a token address.
741 | /// @param token The token to update.
742 | /// @param productId The new Vertex product ID to represent this token.
743 | function updateToken(address token, uint32 productId) external onlyOwner {
744 | // Update the token to product ID and opposite direction mapping.
745 | tokenToProduct[token] = productId;
746 | productToToken[productId] = token;
747 |
748 | emit TokenUpdated(token, productId);
749 | }
750 |
751 | /// @notice Rescues any stuck tokens in the contract.
752 | /// @param token The token to rescue.
753 | /// @param amount The amount of token to rescue.
754 | function rescue(address token, uint256 amount) external onlyOwner {
755 | IERC20Metadata(token).safeTransfer(owner(), amount);
756 | }
757 |
758 | /// @notice Updates the Processor implementation address.
759 | /// @param _processor The new Processor implementation address.
760 | function updateProcessor(address _processor) external onlyOwner {
761 | processor = _processor;
762 | }
763 |
764 | /// @notice Update the quote token.
765 | /// @param _quoteToken The new quote token.
766 | function updateQuoteToken(address _quoteToken) external onlyOwner {
767 | oldQuoteToken = address(quoteToken);
768 | quoteToken = IERC20Metadata(_quoteToken);
769 | }
770 |
771 | /*//////////////////////////////////////////////////////////////
772 | INTERNAL FUNCTIONS
773 | //////////////////////////////////////////////////////////////*/
774 |
775 | /// @dev Upgrades the implementation of the proxy to new address.
776 | function _authorizeUpgrade(address) internal override onlyOwner {}
777 | }
778 |
--------------------------------------------------------------------------------
/src/VertexProcessor.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
5 | import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
6 | import {Math} from "openzeppelin/utils/math/Math.sol";
7 | import {ReentrancyGuard} from "openzeppelin/security/ReentrancyGuard.sol";
8 |
9 | import {Initializable} from "openzeppelin-upgradeable/proxy/utils/Initializable.sol";
10 | import {UUPSUpgradeable} from "openzeppelin-upgradeable/proxy/utils/UUPSUpgradeable.sol";
11 | import {OwnableUpgradeable} from "openzeppelin-upgradeable/access/OwnableUpgradeable.sol";
12 |
13 | import {IEndpoint} from "src/interfaces/IEndpoint.sol";
14 | import {IClearinghouse} from "src/interfaces/IClearinghouse.sol";
15 |
16 | import {VertexStorage} from "src/VertexStorage.sol";
17 | import {VertexManager} from "src/VertexManager.sol";
18 | import {VertexRouter} from "src/VertexRouter.sol";
19 |
20 | /// @title Elixir pool processor for Vertex
21 | /// @author The Elixir Team
22 | /// @custom:security-contact security@elixir.finance
23 | /// @notice Back-end contract to process queue deposits and withdrawals. This contract is delegatecalled from VertexManager.
24 | contract VertexProcessor is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuard, VertexStorage {
25 | using SafeERC20 for IERC20Metadata;
26 |
27 | /*//////////////////////////////////////////////////////////////
28 | EVENTS
29 | //////////////////////////////////////////////////////////////*/
30 |
31 | /// @notice Emitted when a deposit is made.
32 | /// @param router The router of the pool deposited to.
33 | /// @param caller The caller of the deposit function, for which tokens are taken from.
34 | /// @param receiver The receiver of the LP balance.
35 | /// @param id The ID of the pool deposting to.
36 | /// @param token The token deposited.
37 | /// @param amount The token amount deposited.
38 | /// @param shares The amount of shares received.
39 | event Deposit(
40 | address indexed router,
41 | address caller,
42 | address indexed receiver,
43 | uint256 indexed id,
44 | address token,
45 | uint256 amount,
46 | uint256 shares
47 | );
48 |
49 | /// @notice Emitted when a withdraw is made.
50 | /// @param router The router of the pool withdrawn from.
51 | /// @param user The user who withdrew.
52 | /// @param tokenId The Vertex product ID of the token withdrawn.
53 | /// @param amount The token amount the user receives.
54 | event Withdraw(address indexed router, address indexed user, uint32 tokenId, uint256 indexed amount);
55 |
56 | /*//////////////////////////////////////////////////////////////
57 | ERRORS
58 | //////////////////////////////////////////////////////////////*/
59 |
60 | /// @notice Emitted when a token is not supported for a pool.
61 | /// @param token The address of the unsupported token.
62 | /// @param id The ID of the pool.
63 | error UnsupportedToken(address token, uint256 id);
64 |
65 | /// @notice Emitted when the queue spot type is invalid.
66 | error InvalidSpotType(Spot spot);
67 |
68 | /// @notice Emitted when the slippage is too high.
69 | /// @param amount The amount of tokens given.
70 | /// @param amountLow The low limit of token amounts.
71 | /// @param amountHigh The high limit of token amounts.
72 | error SlippageTooHigh(uint256 amount, uint256 amountLow, uint256 amountHigh);
73 |
74 | /// @notice Emitted when the hardcap of a pool would be exceeded.
75 | /// @param token The token address being deposited.
76 | /// @param hardcap The hardcap of the pool given the token.
77 | /// @param activeAmount The active amount of tokens in the pool.
78 | /// @param amount The amount of tokens being deposited.
79 | error HardcapReached(address token, uint256 hardcap, uint256 activeAmount, uint256 amount);
80 |
81 | /*//////////////////////////////////////////////////////////////
82 | INTERNAL DEPOSIT/WITHDRAW LOGIC
83 | //////////////////////////////////////////////////////////////*/
84 |
85 | /// @notice Internal deposit logic for both spot and perp pools.
86 | /// @param caller The user who is depositing.
87 | /// @param id The id of the pool.
88 | /// @param pool The data of the pool to deposit.
89 | /// @param token The tokens to deposit.
90 | /// @param amount The amounts of token to deposit.
91 | /// @param receiver The receiver of the virtual LP balance.
92 | function _deposit(
93 | address caller,
94 | uint256 id,
95 | Pool storage pool,
96 | address token,
97 | uint256 amount,
98 | uint256 shares,
99 | address receiver
100 | ) private {
101 | // Establish empty token data.
102 | Token storage tokenData;
103 |
104 | // If token is the Clearinghouse quote token, point to the old quote token data.
105 | if (oldQuoteToken != address(0) && token == address(quoteToken)) {
106 | tokenData = pool.tokens[oldQuoteToken];
107 | } else {
108 | tokenData = pool.tokens[token];
109 | }
110 |
111 | // Check that the token is supported by the pool.
112 | if (!tokenData.isActive || token == oldQuoteToken) revert UnsupportedToken(token, id);
113 |
114 | // Check if the amount exceeds the token's pool hardcap.
115 | if (tokenData.activeAmount + shares > tokenData.hardcap) {
116 | revert HardcapReached(token, tokenData.hardcap, tokenData.activeAmount, shares);
117 | }
118 |
119 | // Fetch the router of the pool.
120 | VertexRouter router = VertexRouter(pool.router);
121 |
122 | // Transfer tokens from the caller to this contract.
123 | IERC20Metadata(token).safeTransferFrom(caller, address(router), amount);
124 |
125 | // Deposit funds to Vertex through router.
126 | router.submitSlowModeDeposit(tokenToProduct[token], uint128(amount), "9O7rUEUljP");
127 |
128 | // Add amount to the active market making balance of the user.
129 | tokenData.userActiveAmount[receiver] += shares;
130 |
131 | // Add amount to the active pool market making balance.
132 | tokenData.activeAmount += shares;
133 |
134 | emit Deposit(address(router), caller, receiver, id, token, amount, shares);
135 | }
136 |
137 | /// @notice Internal withdraw logic for both spot and perp pools.
138 | /// @param caller The user who is withdrawing.
139 | /// @param pool The data of the pool to withdraw from.
140 | /// @param token The token to withdraw.
141 | /// @param amount The amount of token to substract from active balances.
142 | /// @param fee The fee to pay.
143 | /// @param amountToReceive The amount of tokens the user receives.
144 | function _withdraw(
145 | address caller,
146 | Pool storage pool,
147 | address token,
148 | uint256 amount,
149 | uint256 fee,
150 | uint256 amountToReceive
151 | ) private {
152 | // Establish empty token data.
153 | Token storage tokenData;
154 |
155 | // If token is the Clearinghouse quote token, point to the old quote token data.
156 | if (oldQuoteToken != address(0) && token == address(quoteToken)) {
157 | tokenData = pool.tokens[oldQuoteToken];
158 | } else {
159 | tokenData = pool.tokens[token];
160 | }
161 |
162 | // Substract amount from the active market making balance of the caller.
163 | tokenData.userActiveAmount[caller] -= amount;
164 |
165 | // Substract amount from the active pool market making balance.
166 | tokenData.activeAmount -= amount;
167 |
168 | // Add fee to the Elixir balance.
169 | tokenData.fees[caller] += fee;
170 |
171 | // Update the user pending balance.
172 | tokenData.userPendingAmount[caller] += (amountToReceive - fee);
173 |
174 | // Create Vertex withdraw payload request.
175 | IEndpoint.WithdrawCollateral memory withdrawPayload = IEndpoint.WithdrawCollateral(
176 | VertexRouter(pool.router).contractSubaccount(), tokenToProduct[token], uint128(amountToReceive), 0
177 | );
178 |
179 | // Fetch payment fee from owner. This can be reimbursed on withdrawals after tokens are received.
180 | quoteToken.safeTransferFrom(owner(), pool.router, slowModeFee);
181 |
182 | // Submit Withdraw slow-mode tx to Vertex.
183 | VertexRouter(pool.router).submitSlowModeTransaction(
184 | abi.encodePacked(uint8(IEndpoint.TransactionType.WithdrawCollateral), abi.encode(withdrawPayload))
185 | );
186 |
187 | emit Withdraw(pool.router, caller, tokenToProduct[token], amountToReceive);
188 | }
189 |
190 | /// @notice Processes a spot transaction given a response.
191 | /// @param spot The spot to process.
192 | /// @param response The response for the spot in queue.
193 | /// @param manager The VertexManager contract calling this function.
194 | function processSpot(Spot calldata spot, bytes memory response, VertexManager manager) public {
195 | if (spot.spotType == SpotType.DepositSpot) {
196 | DepositSpot memory spotTxn = abi.decode(spot.transaction, (DepositSpot));
197 |
198 | DepositSpotResponse memory responseTxn = abi.decode(response, (DepositSpotResponse));
199 |
200 | // Check for slippage based on the needed amount1.
201 | if (responseTxn.amount1 < spotTxn.amount1Low || responseTxn.amount1 > spotTxn.amount1High) {
202 | revert SlippageTooHigh(responseTxn.amount1, spotTxn.amount1Low, spotTxn.amount1High);
203 | }
204 |
205 | // Execute deposit logic for token0.
206 | _deposit(
207 | spot.sender,
208 | spotTxn.id,
209 | pools[spotTxn.id],
210 | spotTxn.token0,
211 | spotTxn.amount0,
212 | responseTxn.token0Shares,
213 | spotTxn.receiver
214 | );
215 |
216 | // Execute deposit logic for token1.
217 | _deposit(
218 | spot.sender,
219 | spotTxn.id,
220 | pools[spotTxn.id],
221 | spotTxn.token1,
222 | responseTxn.amount1,
223 | responseTxn.token1Shares,
224 | spotTxn.receiver
225 | );
226 | } else if (spot.spotType == SpotType.DepositPerp) {
227 | DepositPerp memory spotTxn = abi.decode(spot.transaction, (DepositPerp));
228 |
229 | DepositPerpResponse memory responseTxn = abi.decode(response, (DepositPerpResponse));
230 |
231 | // Execute the deposit logic.
232 | _deposit(
233 | spot.sender,
234 | spotTxn.id,
235 | pools[spotTxn.id],
236 | spotTxn.token,
237 | spotTxn.amount,
238 | responseTxn.shares,
239 | spotTxn.receiver
240 | );
241 | } else if (spot.spotType == SpotType.WithdrawPerp) {
242 | WithdrawPerp memory spotTxn = abi.decode(spot.transaction, (WithdrawPerp));
243 |
244 | WithdrawPerpResponse memory responseTxn = abi.decode(response, (WithdrawPerpResponse));
245 |
246 | // Execute the withdraw logic.
247 | _withdraw(
248 | spot.sender,
249 | pools[spotTxn.id],
250 | spotTxn.token,
251 | spotTxn.amount,
252 | manager.getTransactionFee(spotTxn.token),
253 | responseTxn.amountToReceive
254 | );
255 | } else if (spot.spotType == SpotType.WithdrawSpot) {
256 | WithdrawSpot memory spotTxn = abi.decode(spot.transaction, (WithdrawSpot));
257 |
258 | WithdrawSpotResponse memory responseTxn = abi.decode(response, (WithdrawSpotResponse));
259 |
260 | // Execute the withdraw logic for token0.
261 | _withdraw(
262 | spot.sender,
263 | pools[spotTxn.id],
264 | spotTxn.token0,
265 | spotTxn.amount0,
266 | manager.getTransactionFee(spotTxn.token0),
267 | responseTxn.amount0ToReceive
268 | );
269 | // Execute the withdraw logic for token1.
270 | _withdraw(
271 | spot.sender,
272 | pools[spotTxn.id],
273 | spotTxn.token1,
274 | responseTxn.amount1,
275 | manager.getTransactionFee(spotTxn.token1),
276 | responseTxn.amount1ToReceive
277 | );
278 | } else {
279 | revert InvalidSpotType(spot);
280 | }
281 | }
282 |
283 | /*//////////////////////////////////////////////////////////////
284 | INTERNAL FUNCTIONS
285 | //////////////////////////////////////////////////////////////*/
286 |
287 | /// @dev Upgrades the implementation of the proxy to new address.
288 | function _authorizeUpgrade(address) internal override onlyOwner {}
289 | }
290 |
--------------------------------------------------------------------------------
/src/VertexRouter.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
5 | import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
6 |
7 | import {IEndpoint} from "src/interfaces/IEndpoint.sol";
8 |
9 | /// @title Elixir pool router for Vertex
10 | /// @author The Elixir Team
11 | /// @custom:security-contact security@elixir.finance
12 | /// @dev This contract is needed because an address can only have one Vertex linked signer at a time,
13 | /// which is incompatible with the VertexManager singleton approach.
14 | /// @notice Pool router contract to send slow-mode transactions to Vertex.
15 | contract VertexRouter {
16 | using SafeERC20 for IERC20Metadata;
17 |
18 | /*//////////////////////////////////////////////////////////////
19 | VARIABLES
20 | //////////////////////////////////////////////////////////////*/
21 |
22 | /// @notice Vertex's Endpoint contract.
23 | IEndpoint public immutable endpoint;
24 |
25 | /// @notice Bytes of this contract's subaccount.
26 | bytes32 public immutable contractSubaccount;
27 |
28 | /// @notice Bytes of the external account's subaccount.
29 | bytes32 public immutable externalSubaccount;
30 |
31 | /// @notice The Manager contract associated with this Router.
32 | address public immutable manager;
33 |
34 | /*//////////////////////////////////////////////////////////////
35 | ERRORS
36 | //////////////////////////////////////////////////////////////*/
37 |
38 | /// @notice Reverts when the sender is not the manager.
39 | error NotManager();
40 |
41 | /*//////////////////////////////////////////////////////////////
42 | MODIFIERS
43 | //////////////////////////////////////////////////////////////*/
44 |
45 | /// @notice Reverts when the sender is not the manager.
46 | modifier onlyManager() {
47 | if (msg.sender != manager) revert NotManager();
48 | _;
49 | }
50 |
51 | /*//////////////////////////////////////////////////////////////
52 | CONSTRUCTOR
53 | //////////////////////////////////////////////////////////////*/
54 |
55 | /// @notice Set the manager, Vertex Endpoint, and subaccounts.
56 | /// @param _endpoint The address of the Vertex Endpoint contract.
57 | /// @param _externalAccount The address of the external account to link to the Vertex Endpoint.
58 | constructor(address _endpoint, address _externalAccount) {
59 | // Set the Manager as the owner.
60 | manager = msg.sender;
61 |
62 | // Set Vertex's endpoint address.
63 | endpoint = IEndpoint(_endpoint);
64 |
65 | // Store this contract's internal and external subaccount.
66 | contractSubaccount = bytes32(uint256(uint160(address(this))) << 96);
67 | externalSubaccount = bytes32(uint256(uint160(_externalAccount)) << 96);
68 | }
69 |
70 | /*//////////////////////////////////////////////////////////////
71 | VERTEX SLOW TRANSACTION
72 | //////////////////////////////////////////////////////////////*/
73 |
74 | /// @notice Submits a slow mode transaction to Vertex.
75 | /// @dev More information about slow mode transactions:
76 | /// https://vertex-protocol.gitbook.io/docs/developer-resources/api/withdrawing-on-chain
77 | /// @param transaction The transaction to submit.
78 | function submitSlowModeTransaction(bytes memory transaction) external onlyManager {
79 | endpoint.submitSlowModeTransaction(transaction);
80 | }
81 |
82 | /// @notice Sends a deposit to Vertex.
83 | /// @dev By calling this function, the sender and msg.sender are the same.
84 | /// @param productId The Vertex product ID.
85 | /// @param amount The amount to deposit.
86 | /// @param referral The Vertex referral code to use for this deposit.
87 | function submitSlowModeDeposit(uint32 productId, uint128 amount, string calldata referral) external onlyManager {
88 | // Send deposit with the last 12 bytes of this router subaccount. Shift left 160 bits, leaving 96 bits i.e. 12 bytes.
89 | endpoint.depositCollateralWithReferral(bytes12(contractSubaccount << 160), productId, amount, referral);
90 | }
91 |
92 | /*//////////////////////////////////////////////////////////////
93 | TOKEN TRANSFER
94 | //////////////////////////////////////////////////////////////*/
95 |
96 | /// @notice Approves Vertex to transfer a token.
97 | /// @param token The token to approve.
98 | function makeApproval(address token) external onlyManager {
99 | // Approve the token transfer.
100 | IERC20Metadata(token).approve(address(endpoint), type(uint256).max);
101 | }
102 |
103 | /// @notice Allow claims from VertexManager contract.
104 | /// @param token The token to transfer.
105 | /// @param amount The amount to transfer.
106 | function claimToken(address token, uint256 amount) external onlyManager {
107 | // Transfer the token to the manager.
108 | IERC20Metadata(token).safeTransfer(manager, amount);
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/VertexStorage.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
5 |
6 | import {IEndpoint} from "src/interfaces/IEndpoint.sol";
7 | import {IVertexManager} from "src/interfaces/IVertexManager.sol";
8 |
9 | /// @title Elixir storage for Vertex
10 | /// @author The Elixir Team
11 | /// @custom:security-contact security@elixir.finance
12 | /// @notice Back-end contract with storage variables.
13 | abstract contract VertexStorage is IVertexManager {
14 | /// @notice The pools managed given an ID.
15 | mapping(uint256 id => Pool pool) public pools;
16 |
17 | /// @notice The Vertex product IDs of token addresses.
18 | mapping(address token => uint32 id) public tokenToProduct;
19 |
20 | /// @notice The token addresses of Vertex product IDs.
21 | mapping(uint32 id => address token) public productToToken;
22 |
23 | /// @notice The queue for Elixir to process.
24 | mapping(uint128 => Spot) public queue;
25 |
26 | /// @notice The queue count.
27 | uint128 public queueCount;
28 |
29 | /// @notice The queue up to.
30 | uint128 public queueUpTo;
31 |
32 | /// @notice The Vertex slow mode fee.
33 | uint256 public slowModeFee = 1000000;
34 |
35 | /// @notice Vertex's Endpoint contract.
36 | IEndpoint public endpoint;
37 |
38 | /// @notice Fee payment token for slow mode transactions through Vertex.
39 | IERC20Metadata public quoteToken;
40 |
41 | /// @notice The pause status of deposits. True if deposits are paused.
42 | bool public depositPaused;
43 |
44 | /// @notice The pause status of withdrawals. True if withdrawals are paused.
45 | bool public withdrawPaused;
46 |
47 | /// @notice The pause status of claims. True if claims are paused.
48 | bool public claimPaused;
49 |
50 | /// @notice Old quote token of Vertex.
51 | address internal oldQuoteToken;
52 |
53 | /// @notice The smart contract to off-load processing logic.
54 | address internal processor;
55 |
56 | /// invariant: poolId always has same router
57 | /// @notice Signer for a given router
58 | mapping(address router => address signer) public routerSigner;
59 | }
60 |
--------------------------------------------------------------------------------
/src/interfaces/IClearinghouse.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | interface IClearinghouse {
5 | /// @notice Retrieve quote ERC20 address.
6 | function getQuote() external view returns (address);
7 |
8 | /// @notice Retrieve the engine of a product.
9 | function getEngineByProduct(uint32 productId) external view returns (address);
10 | }
11 |
--------------------------------------------------------------------------------
/src/interfaces/IEndpoint.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import {IClearinghouse} from "src/interfaces/IClearinghouse.sol";
5 |
6 | interface IEndpoint {
7 | enum TransactionType {
8 | LiquidateSubaccount,
9 | DepositCollateral,
10 | WithdrawCollateral,
11 | SpotTick,
12 | UpdatePrice,
13 | SettlePnl,
14 | MatchOrders,
15 | DepositInsurance,
16 | ExecuteSlowMode,
17 | MintLp,
18 | BurnLp,
19 | SwapAMM,
20 | MatchOrderAMM,
21 | DumpFees,
22 | ClaimSequencerFees,
23 | PerpTick,
24 | ManualAssert,
25 | Rebate,
26 | UpdateProduct,
27 | LinkSigner,
28 | UpdateFeeRates
29 | }
30 |
31 | struct DepositCollateral {
32 | bytes32 sender;
33 | uint32 productId;
34 | uint128 amount;
35 | }
36 |
37 | struct WithdrawCollateral {
38 | bytes32 sender;
39 | uint32 productId;
40 | uint128 amount;
41 | uint64 nonce;
42 | }
43 |
44 | struct LinkSigner {
45 | bytes32 sender;
46 | bytes32 signer;
47 | uint64 nonce;
48 | }
49 |
50 | struct SlowModeTx {
51 | uint64 executableAt;
52 | address sender;
53 | bytes tx;
54 | }
55 |
56 | /// @notice Returns the Clearinghouse contract.
57 | function clearinghouse() external view returns (IClearinghouse);
58 |
59 | /// @notice Returns the slow-mode queue state.
60 | function getSlowModeTx(uint64 idx) external view returns (SlowModeTx memory, uint64, uint64);
61 |
62 | /// @notice Executes a submitted slow-mode transaction.
63 | function executeSlowModeTransaction() external;
64 |
65 | /// @notice Submits a slow-mode transaction to Vertex.
66 | function submitSlowModeTransaction(bytes calldata transaction) external;
67 |
68 | /// @notice Submits a deposit transaction to Vertex.
69 | function depositCollateralWithReferral(
70 | bytes12 subaccountName,
71 | uint32 productId,
72 | uint128 amount,
73 | string calldata referralCode
74 | ) external;
75 |
76 | /// @notice Returns a slow-mode transaction.
77 | function slowModeTxs(uint64 txId) external view returns (uint64 executableAt, address sender, bytes calldata tx);
78 |
79 | /// @notice Gets the price of a product.
80 | function getPriceX18(uint32 productId) external view returns (uint256);
81 | }
82 |
--------------------------------------------------------------------------------
/src/interfaces/IVertexManager.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | interface IVertexManager {
5 | /// @notice The types of spots supported by this contract.
6 | enum SpotType {
7 | Empty,
8 | DepositSpot,
9 | WithdrawPerp,
10 | WithdrawSpot,
11 | DepositPerp
12 | }
13 |
14 | /// @notice The types of queue-related events.
15 | enum QueueEvent {
16 | Deposit,
17 | Withdraw,
18 | Unqueue
19 | }
20 |
21 | /// @notice The structure for perp deposits to be processed by Elixir.
22 | struct DepositPerp {
23 | // The ID of the pool.
24 | uint256 id;
25 | // The token address.
26 | address token;
27 | // The amount of token to deposit.
28 | uint256 amount;
29 | // The receiver address.
30 | address receiver;
31 | }
32 |
33 | /// @notice The structure for spot deposits to be processed by Elixir.
34 | struct DepositSpot {
35 | // The ID of the pool.
36 | uint256 id;
37 | // The token0 address.
38 | address token0;
39 | // The token1 address.
40 | address token1;
41 | // The amount of token0 to deposit.
42 | uint256 amount0;
43 | // The low limit of token1 to deposit.
44 | uint256 amount1Low;
45 | // The high limit of token1 to deposit.
46 | uint256 amount1High;
47 | // The receiver of the virtual LP balance.
48 | address receiver;
49 | }
50 |
51 | /// @notice The structure of perp withdrawals to be processed by Elixir.
52 | struct WithdrawPerp {
53 | // The ID of the pool.
54 | uint256 id;
55 | // The token address.
56 | address token;
57 | // The amount of token shares to withdraw.
58 | uint256 amount;
59 | }
60 |
61 | /// @notice The structure of spot withdrawals to be processed by Elixir.
62 | struct WithdrawSpot {
63 | // The ID of the pool.
64 | uint256 id;
65 | // The token0 address.
66 | address token0;
67 | // The token1 address.
68 | address token1;
69 | // The amount of token0 shares to withdraw.
70 | uint256 amount0;
71 | }
72 |
73 | /// @notice The response structure for DepositSpot.
74 | struct DepositSpotResponse {
75 | // The amount of token1 to take from user.
76 | uint256 amount1;
77 | // The amount of token0 shares to add.
78 | uint256 token0Shares;
79 | // The amount of token1 shares to add.
80 | uint256 token1Shares;
81 | }
82 |
83 | /// @notice The response structure for DepositPerp.
84 | struct DepositPerpResponse {
85 | // The amount of shares to receive.
86 | uint256 shares;
87 | }
88 |
89 | /// @notice The response structure for WithdrawPerp.
90 | struct WithdrawPerpResponse {
91 | // The amount of of tokens the user should receive.
92 | uint256 amountToReceive;
93 | }
94 |
95 | /// @notice The response structure for WithdrawSpot.
96 | struct WithdrawSpotResponse {
97 | // The amount of token1 to use.
98 | uint256 amount1;
99 | // The amount of token0 the user should receive.
100 | uint256 amount0ToReceive;
101 | // The amount of token1 the user should receive.
102 | uint256 amount1ToReceive;
103 | }
104 |
105 | /// @notice The types of pools supported by this contract.
106 | enum PoolType {
107 | Inactive,
108 | Spot,
109 | Perp
110 | }
111 |
112 | /// @notice The data structure of pools.
113 | struct Pool {
114 | // The router address of the pool.
115 | address router;
116 | // The pool type. True for spot, false for perp.
117 | PoolType poolType;
118 | // The data of the supported tokens in the pool.
119 | mapping(address token => Token data) tokens;
120 | }
121 |
122 | /// @notice The data structure of tokens.
123 | struct Token {
124 | // The active market making balance of users for a token within a pool.
125 | mapping(address user => uint256 balance) userActiveAmount;
126 | // The pending amounts of users for a token within a pool.
127 | mapping(address user => uint256 amount) userPendingAmount;
128 | // The pending fees of a token within a pool.
129 | mapping(address user => uint256 amount) fees;
130 | // The total active amounts of a token within a pool.
131 | uint256 activeAmount;
132 | // The hardcap of the token within a pool.
133 | uint256 hardcap;
134 | // The status of the token within a pool. True if token is supported.
135 | bool isActive;
136 | }
137 |
138 | /// @notice The data structure of queue spots.
139 | struct Spot {
140 | // The sender of the request.
141 | address sender;
142 | // The router address of the pool.
143 | address router;
144 | // The type of request.
145 | SpotType spotType;
146 | // The transaction to process.
147 | bytes transaction;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/test/Distributor.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
7 |
8 | import {Distributor} from "src/Distributor.sol";
9 |
10 | import {MockToken} from "test/utils/MockToken.sol";
11 |
12 | contract TestDistributor is Test {
13 | /*//////////////////////////////////////////////////////////////
14 | CONTRACTS
15 | //////////////////////////////////////////////////////////////*/
16 |
17 | MockToken public token;
18 | Distributor public rewards;
19 |
20 | /*//////////////////////////////////////////////////////////////
21 | USERS
22 | //////////////////////////////////////////////////////////////*/
23 |
24 | // Elixir signer
25 | address public signer;
26 |
27 | /*//////////////////////////////////////////////////////////////
28 | MISC
29 | //////////////////////////////////////////////////////////////*/
30 |
31 | // Random private key of signer.
32 | uint256 public privateKey = 0x12345;
33 |
34 | // EIP712 domain hash.
35 | bytes32 public eip712DomainHash;
36 |
37 | // cast keccak "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
38 | bytes32 public constant TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
39 |
40 | // cast keccak "Claim(address user,address token,uint256 amount,uint256 nonce)"
41 | bytes32 public constant CLAIM_TYPEHASH = 0xc842860edd57fc9c0a15e879d1fed9e117378a23423d6e17899e5106ca1eb849;
42 |
43 | struct Claim {
44 | address user;
45 | address token;
46 | uint256 amount;
47 | uint256 nonce;
48 | }
49 |
50 | /*//////////////////////////////////////////////////////////////
51 | HELPERS
52 | //////////////////////////////////////////////////////////////*/
53 |
54 | function setUp() public {
55 | // Set the signer.
56 | signer = vm.addr(privateKey);
57 |
58 | // Deploy token.
59 | token = new MockToken();
60 |
61 | // Deploy contract.
62 | rewards = new Distributor("Distributor", "1", signer);
63 |
64 | // Set the domain hash.
65 | eip712DomainHash = keccak256(
66 | abi.encode(
67 | TYPEHASH, keccak256(bytes("Distributor")), keccak256(bytes("1")), block.chainid, address(rewards)
68 | )
69 | );
70 | }
71 |
72 | // Computes the hash of a claim.
73 | function getStructHash(Claim memory _claim) internal pure returns (bytes32) {
74 | return keccak256(abi.encode(CLAIM_TYPEHASH, _claim.user, _claim.token, _claim.amount, _claim.nonce));
75 | }
76 |
77 | // Computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer
78 | function getTypedDataHash(Claim memory _claim) public view returns (bytes32) {
79 | return keccak256(abi.encodePacked("\x19\x01", eip712DomainHash, getStructHash(_claim)));
80 | }
81 |
82 | function generateSignature(Claim memory claim) public returns (bytes memory signature) {
83 | bytes32 digest = getTypedDataHash(claim);
84 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
85 |
86 | signature = abi.encodePacked(r, s, v);
87 |
88 | assertEq(signature.length, 65);
89 | }
90 |
91 | /*//////////////////////////////////////////////////////////////
92 | TESTS
93 | //////////////////////////////////////////////////////////////*/
94 |
95 | function testDoubleClaim(uint128 amount, uint256 nonce) public {
96 | // Skip zeros.
97 | vm.assume(amount > 0 && amount <= type(uint128).max && nonce > 0 && nonce <= type(uint256).max - 1);
98 |
99 | // Mint tokens to contract.
100 | token.mint(address(rewards), amount);
101 |
102 | // Generate message to sign.
103 | Claim memory claim = Claim({user: address(this), token: address(token), amount: amount, nonce: nonce});
104 |
105 | assertEq(token.balanceOf(address(rewards)), amount);
106 |
107 | rewards.claim(address(this), address(token), amount, nonce, generateSignature(claim));
108 |
109 | assertEq(token.balanceOf(address(rewards)), 0);
110 |
111 | // Generate anonther message to sign.
112 | Claim memory claim2 = Claim({user: address(this), token: address(token), amount: amount, nonce: nonce + 1});
113 |
114 | // Mint tokens to contract.
115 | token.mint(address(rewards), amount);
116 |
117 | assertEq(token.balanceOf(address(rewards)), amount);
118 |
119 | rewards.claim(address(this), address(token), amount, nonce + 1, generateSignature(claim2));
120 |
121 | assertEq(token.balanceOf(address(rewards)), 0);
122 | }
123 |
124 | function testAlreadyClaimed() public {
125 | token.mint(address(rewards), 100 ether);
126 |
127 | Claim memory claim = Claim({user: address(this), token: address(token), amount: 100 ether, nonce: 1});
128 |
129 | bytes memory signature = generateSignature(claim);
130 |
131 | assertEq(token.balanceOf(address(rewards)), 100 ether);
132 |
133 | rewards.claim(address(this), address(token), 100 ether, 1, signature);
134 |
135 | assertEq(token.balanceOf(address(rewards)), 0);
136 |
137 | vm.expectRevert(abi.encodeWithSelector(Distributor.AlreadyClaimed.selector));
138 | rewards.claim(address(this), address(token), 100 ether, 1, signature);
139 |
140 | assertEq(token.balanceOf(address(rewards)), 0);
141 | }
142 |
143 | function testInvalid() public {
144 | vm.expectRevert(abi.encodeWithSelector(Distributor.InvalidToken.selector));
145 | rewards.claim(address(this), address(0), 0, 0, bytes(""));
146 |
147 | vm.expectRevert(abi.encodeWithSelector(Distributor.InvalidAmount.selector));
148 | rewards.claim(address(this), address(token), 0, 1, bytes(""));
149 |
150 | vm.expectRevert(abi.encodeWithSelector(Distributor.InvalidNonce.selector));
151 | rewards.claim(address(this), address(token), 1, 0, bytes(""));
152 |
153 | Claim memory claim = Claim({user: address(this), token: address(token), amount: 1 ether, nonce: 1});
154 |
155 | bytes32 digest = getTypedDataHash(claim);
156 |
157 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(0x123, digest);
158 |
159 | vm.expectRevert(abi.encodeWithSelector(Distributor.InvalidSignature.selector));
160 | rewards.claim(address(this), address(token), 1 ether, 1, abi.encodePacked(r, s, v));
161 | }
162 |
163 | function testNotUser() public {
164 | token.mint(address(rewards), 100 ether);
165 |
166 | Claim memory claim = Claim({user: address(0xbeef), token: address(token), amount: 100 ether, nonce: 1});
167 |
168 | bytes memory signature = generateSignature(claim);
169 |
170 | assertEq(token.balanceOf(address(rewards)), 100 ether);
171 |
172 | vm.expectRevert(abi.encodeWithSelector(Distributor.InvalidSignature.selector));
173 | rewards.claim(address(this), address(token), 100 ether, 1, signature);
174 |
175 | assertEq(token.balanceOf(address(rewards)), 100 ether);
176 | }
177 |
178 | function testNotEnough() public {
179 | Claim memory claim = Claim({user: address(this), token: address(token), amount: 100 ether, nonce: 1});
180 |
181 | bytes memory signature = generateSignature(claim);
182 |
183 | vm.expectRevert("ERC20: transfer amount exceeds balance");
184 | rewards.claim(address(this), address(token), 100 ether, 1, signature);
185 | }
186 |
187 | function testWithdraw() public {
188 | token.mint(address(rewards), 100 ether);
189 |
190 | assertEq(token.balanceOf(address(rewards)), 100 ether);
191 |
192 | rewards.emergencyWithdraw(address(token), 100 ether);
193 |
194 | assertEq(token.balanceOf(address(rewards)), 0);
195 | }
196 |
197 | function testNotOwner() public {
198 | vm.prank(address(0xbeef));
199 | vm.expectRevert("Ownable: caller is not the owner");
200 | rewards.emergencyWithdraw(address(0), 0);
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/test/VertexManagerUpgrade.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import {ProcessQueue} from "test/utils/ProcessQueue.sol";
7 |
8 | import {VertexManager, IVertexManager} from "src/VertexManager.sol";
9 | import {VertexProcessor} from "src/VertexProcessor.sol";
10 | import {VertexRouter} from "src/VertexRouter.sol";
11 | import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
12 |
13 | contract TestVertexManagerUpgrade is Test, ProcessQueue {
14 | VertexManager internal manager;
15 |
16 | IERC20 BTC = IERC20(0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f);
17 | IERC20 USDC = IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831);
18 | IERC20 WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
19 |
20 | uint256 public networkFork;
21 |
22 | // RPC URL for Arbitrum fork.
23 | string public networkRpcUrl = vm.envString("ARBITRUM_RPC_URL");
24 |
25 | function testUpgradeFork() external {
26 | networkFork = vm.createFork(networkRpcUrl);
27 |
28 | vm.selectFork(networkFork);
29 |
30 | // Wrap in ABI to support easier calls.
31 | manager = VertexManager(0x052Ab3fd33cADF9D9f227254252da3f996431f75);
32 |
33 | // Deploy with key.
34 | vm.startPrank(manager.owner());
35 |
36 | // Get the endpoint address before upgrading.
37 | address endpoint = address(manager.endpoint());
38 |
39 | // Deploy new Processor implementation.
40 | VertexProcessor newProcessor = new VertexProcessor();
41 |
42 | // Deploy new Manager implementation.
43 | VertexManager newManager = new VertexManager();
44 |
45 | // Upgrade proxy to new implementation.
46 | manager.upgradeToAndCall(
47 | address(newManager), abi.encodeWithSelector(VertexManager.updateProcessor.selector, address(newProcessor))
48 | );
49 |
50 | // Check upgrade by ensuring storage is not changed.
51 | require(address(manager.endpoint()) == endpoint, "Invalid upgrade");
52 |
53 | // Increase hardcap
54 | uint256[] memory hardcaps = new uint256[](2);
55 | hardcaps[0] = type(uint256).max;
56 | hardcaps[1] = type(uint256).max;
57 |
58 | address[] memory spotTokens = new address[](2);
59 | spotTokens[0] = address(BTC);
60 | spotTokens[1] = address(USDC);
61 |
62 | manager.updatePoolHardcaps(1, spotTokens, hardcaps);
63 |
64 | uint256[] memory ids = new uint256[](1);
65 | address[] memory signers = new address[](1);
66 |
67 | ids[0] = 1;
68 | signers[0] = 0xD7cb7F791bb97A1a8B5aFc3aec5fBD0BEC4536A5;
69 |
70 | manager.updateLinkedSigners(ids, signers);
71 | vm.stopPrank();
72 |
73 | uint256 amountBTC = 10 * 10 ** 8;
74 | uint256 amountUSDC = manager.getBalancedAmount(address(BTC), address(USDC), amountBTC);
75 |
76 | deal(address(BTC), address(this), amountBTC + manager.getTransactionFee(address(BTC)));
77 | deal(address(USDC), address(this), amountUSDC);
78 |
79 | BTC.approve(address(manager), amountBTC + manager.getTransactionFee(address(BTC)));
80 | USDC.approve(address(manager), amountUSDC);
81 |
82 | uint256 fee = manager.getTransactionFee(address(WETH));
83 |
84 | manager.depositSpot{value: fee}(
85 | 1, address(BTC), address(USDC), amountBTC, amountUSDC, amountUSDC, address(this)
86 | );
87 |
88 | // Get the router address
89 | (address router,,,) = manager.getPoolToken(1, address(BTC));
90 |
91 | vm.startPrank(address(uint160(bytes20(VertexRouter(router).externalSubaccount()))));
92 | processQueue(manager);
93 | vm.stopPrank();
94 |
95 | uint256 userActiveAmountBTC = manager.getUserActiveAmount(1, address(BTC), address(this));
96 | uint256 userActiveAmountUSDC = manager.getUserActiveAmount(1, address(USDC), address(this));
97 |
98 | assertEq(userActiveAmountBTC, amountBTC);
99 | assertEq(userActiveAmountUSDC, amountUSDC);
100 | }
101 |
102 | // Exclude from coverage report
103 | function test() public {}
104 | }
105 |
--------------------------------------------------------------------------------
/test/invariants/VertexManager.invariants.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import {MockToken} from "test/utils/MockToken.sol";
7 |
8 | import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
9 | import {ERC1967Proxy} from "openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
10 | import {Math} from "openzeppelin/utils/math/Math.sol";
11 |
12 | import {IEndpoint} from "src/interfaces/IEndpoint.sol";
13 |
14 | import {VertexManager, IVertexManager} from "src/VertexManager.sol";
15 | import {VertexProcessor} from "src/VertexProcessor.sol";
16 | import {Handler} from "test/invariants/VertexManagerHandler.sol";
17 |
18 | contract TestInvariantsVertexManager is Test {
19 | using Math for uint256;
20 |
21 | /*//////////////////////////////////////////////////////////////
22 | VARIABLES
23 | //////////////////////////////////////////////////////////////*/
24 |
25 | // Vertex contracts
26 | IEndpoint public endpoint = IEndpoint(0xbbEE07B3e8121227AfCFe1E2B82772246226128e);
27 |
28 | // Elixir contracts
29 | VertexManager public manager;
30 |
31 | // Tokens
32 | IERC20Metadata public BTC = IERC20Metadata(0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f);
33 | IERC20Metadata public USDC = IERC20Metadata(0xaf88d065e77c8cC2239327C5EDb3A432268e5831);
34 | IERC20Metadata public WETH = IERC20Metadata(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
35 |
36 | uint256 public BTC_TOTAL;
37 | uint256 public USDC_TOTAL;
38 | uint256 public WETH_TOTAL;
39 |
40 | // Handler
41 | Handler public handler;
42 |
43 | /*//////////////////////////////////////////////////////////////
44 | MISC
45 | //////////////////////////////////////////////////////////////*/
46 |
47 | // Pool types.
48 | enum PoolType {
49 | Spot,
50 | Perp
51 | }
52 |
53 | /*//////////////////////////////////////////////////////////////
54 | HELPERS
55 | //////////////////////////////////////////////////////////////*/
56 |
57 | function setUp() public {
58 | uint256 networkFork = vm.createFork(vm.envString("ARBITRUM_RPC_URL"), 188383571);
59 |
60 | vm.selectFork(networkFork);
61 |
62 | // Create perp pool with BTC, USDC, and ETH as tokens.
63 | address[] memory perpTokens = new address[](3);
64 | perpTokens[0] = address(BTC);
65 | perpTokens[1] = address(USDC);
66 | perpTokens[2] = address(WETH);
67 |
68 | // Create spot pool with BTC (base) and USDC (quote) as tokens.
69 | address[] memory spotTokens = new address[](2);
70 | spotTokens[0] = address(BTC);
71 | spotTokens[1] = address(USDC);
72 |
73 | uint256[] memory spotHardcaps = new uint256[](2);
74 | spotHardcaps[0] = type(uint256).max;
75 | spotHardcaps[1] = type(uint256).max;
76 |
77 | uint256[] memory perpHardcaps = new uint256[](3);
78 | perpHardcaps[0] = type(uint256).max;
79 | perpHardcaps[1] = type(uint256).max;
80 | perpHardcaps[2] = type(uint256).max;
81 |
82 | // Deploy Processor implementation
83 | VertexProcessor processorImplementation = new VertexProcessor();
84 |
85 | // Deploy Manager implementation
86 | VertexManager managerImplementation = new VertexManager();
87 |
88 | // Deploy and initialize the proxy contract.
89 | ERC1967Proxy proxy = new ERC1967Proxy(
90 | address(managerImplementation),
91 | abi.encodeWithSignature(
92 | "initialize(address,address,uint256)", address(endpoint), address(processorImplementation), 1000000
93 | )
94 | );
95 |
96 | // Wrap in ABI to support easier calls
97 | manager = VertexManager(address(proxy));
98 |
99 | // Wrap into the handler.
100 | handler = new Handler(manager, spotTokens, perpTokens, address(this));
101 |
102 | // Approve the manager to move USDC for fee payments.
103 | USDC.approve(address(manager), type(uint256).max);
104 |
105 | // Deal payment token to the owner, which pays for the slow mode transactions of the pools. No update to the totalSupply.
106 | deal(address(USDC), address(this), type(uint128).max);
107 |
108 | // Add perp pool.
109 | manager.addPool(2, perpTokens, perpHardcaps, IVertexManager.PoolType.Perp, address(this));
110 |
111 | // Add spot pool.
112 | manager.addPool(1, spotTokens, spotHardcaps, IVertexManager.PoolType.Spot, address(this));
113 |
114 | // Add token support.
115 | manager.updateToken(address(USDC), 0);
116 | manager.updateToken(address(BTC), 1);
117 | manager.updateToken(address(WETH), 3);
118 |
119 | // Set the total supplies.
120 | BTC_TOTAL = BTC.totalSupply();
121 | USDC_TOTAL = USDC.totalSupply();
122 | WETH_TOTAL = WETH.totalSupply();
123 |
124 | // Mint tokens.
125 | deal(address(BTC), address(handler), BTC_TOTAL, true);
126 | deal(address(USDC), address(handler), USDC_TOTAL, true);
127 | deal(address(WETH), address(handler), WETH_TOTAL, true);
128 |
129 | // Select the selectors to use for fuzzing.
130 | bytes4[] memory selectors = new bytes4[](6);
131 | selectors[0] = Handler.depositPerp.selector;
132 | selectors[1] = Handler.depositSpot.selector;
133 | selectors[2] = Handler.withdrawSpot.selector;
134 | selectors[3] = Handler.withdrawPerp.selector;
135 | selectors[4] = Handler.claimPerp.selector;
136 | selectors[5] = Handler.claimSpot.selector;
137 |
138 | // Exclude invalid senders.
139 | excludeSender(address(handler));
140 | excludeSender(address(USDC));
141 | excludeSender(address(this));
142 |
143 | // Set the target selector.
144 | targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
145 |
146 | // Set the target contract.
147 | targetContract(address(handler));
148 | }
149 |
150 | /*//////////////////////////////////////////////////////////////
151 | DEPOSIT/WITHDRAWAL INVARIANT TESTS
152 | //////////////////////////////////////////////////////////////*/
153 |
154 | // The sum of the Handler's balances, the active amounts, and the pending amounts should always equal the total amount given.
155 | // Aditionally, the total amounts given must match the total supply of the tokens.
156 | function invariant_conservationOfTokens() public {
157 | // Active amounts
158 | (, uint256 spotActiveAmountBTC,,) = manager.getPoolToken(1, address(BTC));
159 | (, uint256 spotActiveAmountUSDC,,) = manager.getPoolToken(1, address(USDC));
160 |
161 | (, uint256 perpActiveAmountBTC,,) = manager.getPoolToken(2, address(BTC));
162 | (, uint256 perpActiveAmountUSDC,,) = manager.getPoolToken(2, address(USDC));
163 | (, uint256 perpActiveAmountWETH,,) = manager.getPoolToken(2, address(WETH));
164 |
165 | // Pending amounts
166 | uint256 pendingAmountBTC = handler.reduceActors(0, this.accumulatePendingBalanceBTC);
167 | uint256 pendingAmountUSDC = handler.reduceActors(0, this.accumulatePendingBalanceUSDC);
168 | uint256 pendingAmountWETH = handler.reduceActors(0, this.accumulatePendingBalanceWETH);
169 |
170 | assertEq(
171 | BTC_TOTAL,
172 | BTC.balanceOf(address(handler)) + spotActiveAmountBTC + perpActiveAmountBTC + pendingAmountBTC
173 | + handler.ghost_fees(address(BTC))
174 | );
175 | assertEq(
176 | USDC_TOTAL,
177 | USDC.balanceOf(address(handler)) + spotActiveAmountUSDC + perpActiveAmountUSDC + pendingAmountUSDC
178 | + handler.ghost_fees(address(USDC))
179 | );
180 | assertEq(
181 | WETH_TOTAL,
182 | WETH.balanceOf(address(handler)) + perpActiveAmountWETH + pendingAmountWETH
183 | + handler.ghost_fees(address(WETH))
184 | );
185 | }
186 |
187 | // The active amounts should always be equal to the sum of individual active balances. Obtained by the ghost values.
188 | function invariant_solvencyDeposits() public {
189 | (, uint256 spotActiveAmountBTC,,) = manager.getPoolToken(1, address(BTC));
190 | (, uint256 spotActiveAmountUSDC,,) = manager.getPoolToken(1, address(USDC));
191 |
192 | (, uint256 perpActiveAmountBTC,,) = manager.getPoolToken(2, address(BTC));
193 | (, uint256 perpActiveAmountUSDC,,) = manager.getPoolToken(2, address(USDC));
194 | (, uint256 perpActiveAmountWETH,,) = manager.getPoolToken(2, address(WETH));
195 |
196 | assertEq(
197 | spotActiveAmountBTC + perpActiveAmountBTC,
198 | handler.ghost_deposits(address(BTC)) - handler.ghost_withdraws(address(BTC))
199 | );
200 | assertEq(
201 | spotActiveAmountUSDC + perpActiveAmountUSDC,
202 | handler.ghost_deposits(address(USDC)) - handler.ghost_withdraws(address(USDC))
203 | );
204 | assertEq(perpActiveAmountWETH, handler.ghost_deposits(address(WETH)) - handler.ghost_withdraws(address(WETH)));
205 | }
206 |
207 | // The active amounts should always be equal to the sum of individual active balances. Obtained by the sum of each user.
208 | function invariant_solvencyBalances() public {
209 | uint256 sumOfActiveBalancesBTC = handler.reduceActors(0, this.accumulateActiveBalanceBTC);
210 | uint256 sumOfActiveBalancesUSDC = handler.reduceActors(0, this.accumulateActiveBalanceUSDC);
211 | uint256 sumOfActiveBalancesWETH = handler.reduceActors(0, this.accumulateActiveBalanceWETH);
212 |
213 | (, uint256 spotActiveAmountBTC,,) = manager.getPoolToken(1, address(BTC));
214 | (, uint256 spotActiveAmountUSDC,,) = manager.getPoolToken(1, address(USDC));
215 |
216 | (, uint256 perpActiveAmountBTC,,) = manager.getPoolToken(2, address(BTC));
217 | (, uint256 perpActiveAmountUSDC,,) = manager.getPoolToken(2, address(USDC));
218 | (, uint256 perpActiveAmountWETH,,) = manager.getPoolToken(2, address(WETH));
219 |
220 | assertEq(spotActiveAmountBTC + perpActiveAmountBTC, sumOfActiveBalancesBTC);
221 | assertEq(spotActiveAmountUSDC + perpActiveAmountUSDC, sumOfActiveBalancesUSDC);
222 | assertEq(perpActiveAmountWETH, sumOfActiveBalancesWETH);
223 | }
224 |
225 | // No individual account balance can exceed the tokens totalSupply().
226 | function invariant_depositorBalances() public {
227 | handler.forEachActor(this.assertAccountBalanceLteTotalSupply);
228 | }
229 |
230 | // The sum of the deposits must always be greater or equal than the sum of withdraws.
231 | function invariant_depositsAndWithdraws() public {
232 | uint256 sumOfDepositsBTC = handler.ghost_deposits(address(BTC));
233 | uint256 sumOfDepositsUSDC = handler.ghost_deposits(address(USDC));
234 | uint256 sumOfDepositsWETH = handler.ghost_deposits(address(WETH));
235 |
236 | uint256 sumOfWithdrawsBTC = handler.ghost_withdraws(address(BTC));
237 | uint256 sumOfWithdrawsUSDC = handler.ghost_withdraws(address(USDC));
238 | uint256 sumOfWithdrawsWETH = handler.ghost_withdraws(address(WETH));
239 |
240 | assertGe(sumOfDepositsBTC, sumOfWithdrawsBTC);
241 | assertGe(sumOfDepositsUSDC, sumOfWithdrawsUSDC);
242 | assertGe(sumOfDepositsWETH, sumOfWithdrawsWETH);
243 | }
244 |
245 | // The sum of ghost withdrawals must be equal to the sum of pending balances, claims and ghost fees.
246 | function invariant_withdrawBalances() public {
247 | uint256 sumOfClaimsBTC = handler.ghost_claims(address(BTC));
248 | uint256 sumOfClaimsUSDC = handler.ghost_claims(address(USDC));
249 | uint256 sumOfClaimsWETH = handler.ghost_claims(address(WETH));
250 |
251 | uint256 sumOfPendingBalancesBTC = handler.reduceActors(0, this.accumulatePendingBalanceBTC);
252 | uint256 sumOfPendingBalancesUSDC = handler.reduceActors(0, this.accumulatePendingBalanceUSDC);
253 | uint256 sumOfPendingBalancesWETH = handler.reduceActors(0, this.accumulatePendingBalanceWETH);
254 |
255 | assertEq(
256 | handler.ghost_withdraws(address(BTC)),
257 | sumOfPendingBalancesBTC + sumOfClaimsBTC + handler.ghost_fees(address(BTC))
258 | );
259 | assertEq(
260 | handler.ghost_withdraws(address(USDC)),
261 | sumOfPendingBalancesUSDC + sumOfClaimsUSDC + handler.ghost_fees(address(USDC))
262 | );
263 | assertEq(
264 | handler.ghost_withdraws(address(WETH)),
265 | sumOfPendingBalancesWETH + sumOfClaimsWETH + handler.ghost_fees(address(WETH))
266 | );
267 | }
268 |
269 | // Two pools cannot share the same router. Each pool must have a unique and constant router for all tokens supported by it.
270 | function invariant_router() public {
271 | (address routerBTC1,,,) = manager.getPoolToken(1, address(BTC));
272 | (address routerUSDC1,,,) = manager.getPoolToken(1, address(USDC));
273 | (address routerBTC2,,,) = manager.getPoolToken(2, address(BTC));
274 | (address routerUSDC2,,,) = manager.getPoolToken(2, address(USDC));
275 | (address routerWETH2,,,) = manager.getPoolToken(2, address(WETH));
276 |
277 | assertEq(routerBTC1, routerUSDC1);
278 | assertEq(routerBTC2, routerUSDC2);
279 | assertEq(routerBTC2, routerWETH2);
280 | assertTrue(routerBTC1 != routerBTC2);
281 | }
282 |
283 | /*//////////////////////////////////////////////////////////////
284 | HELPERS
285 | //////////////////////////////////////////////////////////////*/
286 |
287 | function assertAccountBalanceLteTotalSupply(address account) external {
288 | uint256 activeAmountBTC = activeAmountUser(address(BTC), account);
289 | uint256 activeAmountUSDC = activeAmountUser(address(USDC), account);
290 | uint256 activeAmountWETH = manager.getUserActiveAmount(2, address(WETH), account);
291 |
292 | assertLe(activeAmountBTC, BTC.totalSupply());
293 | assertLe(activeAmountUSDC, USDC.totalSupply());
294 | assertLe(activeAmountWETH, WETH.totalSupply());
295 | }
296 |
297 | function accumulateActiveBalanceBTC(uint256 balance, address caller) external view returns (uint256) {
298 | return balance + activeAmountUser(address(BTC), caller);
299 | }
300 |
301 | function accumulateActiveBalanceUSDC(uint256 balance, address caller) external view returns (uint256) {
302 | return balance + activeAmountUser(address(USDC), caller);
303 | }
304 |
305 | function accumulateActiveBalanceWETH(uint256 balance, address caller) external view returns (uint256) {
306 | return balance + manager.getUserActiveAmount(2, address(WETH), caller);
307 | }
308 |
309 | function accumulatePendingBalanceBTC(uint256 balance, address caller) external view returns (uint256) {
310 | return balance + manager.getUserPendingAmount(1, address(BTC), caller)
311 | + manager.getUserPendingAmount(2, address(BTC), caller);
312 | }
313 |
314 | function accumulatePendingBalanceUSDC(uint256 balance, address caller) external view returns (uint256) {
315 | return balance + manager.getUserPendingAmount(1, address(USDC), caller)
316 | + manager.getUserPendingAmount(2, address(USDC), caller);
317 | }
318 |
319 | function accumulatePendingBalanceWETH(uint256 balance, address caller) external view returns (uint256) {
320 | return balance + manager.getUserPendingAmount(2, address(WETH), caller);
321 | }
322 |
323 | function activeAmountUser(address token, address user) public view returns (uint256) {
324 | return manager.getUserActiveAmount(1, token, user) + manager.getUserActiveAmount(2, token, user);
325 | }
326 |
327 | receive() external payable {}
328 |
329 | // Exclude from coverage report
330 | function test() public {}
331 | }
332 |
--------------------------------------------------------------------------------
/test/invariants/VertexManagerHandler.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import {CommonBase} from "forge-std/Base.sol";
5 | import {StdCheats} from "forge-std/StdCheats.sol";
6 | import {StdUtils} from "forge-std/StdUtils.sol";
7 | import {console} from "forge-std/console.sol";
8 |
9 | import {ProcessQueue} from "test/utils/ProcessQueue.sol";
10 | import {AddressSet, LibAddressSet} from "test/utils/AddressSet.sol";
11 | import {MockToken} from "test/utils/MockToken.sol";
12 |
13 | import {VertexManager, IVertexManager} from "src/VertexManager.sol";
14 |
15 | import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
16 |
17 | contract Handler is ProcessQueue {
18 | using LibAddressSet for AddressSet;
19 |
20 | /*//////////////////////////////////////////////////////////////
21 | VARIABLES
22 | //////////////////////////////////////////////////////////////*/
23 |
24 | // Elixir contracts
25 | VertexManager public manager;
26 |
27 | // Elixir external account
28 | address public externalAccount;
29 |
30 | // Tokens
31 | IERC20Metadata public BTC;
32 | IERC20Metadata public USDC;
33 | IERC20Metadata public WETH;
34 |
35 | // Ghost balances
36 | mapping(address => uint256) public ghost_deposits;
37 | mapping(address => uint256) public ghost_withdraws;
38 | mapping(address => uint256) public ghost_fees;
39 | mapping(address => uint256) public ghost_claims;
40 |
41 | // Current actor
42 | address public currentActor;
43 |
44 | // Actors
45 | AddressSet internal _actors;
46 |
47 | // Spot tokens
48 | address[] public spotTokens;
49 |
50 | // Perp tokens
51 | address[] public perpTokens;
52 |
53 | // Elixir fee
54 | uint256 public fee;
55 |
56 | /*//////////////////////////////////////////////////////////////
57 | MODIFIERS
58 | //////////////////////////////////////////////////////////////*/
59 |
60 | modifier createActor() {
61 | if (msg.sender == address(USDC)) return;
62 | currentActor = msg.sender;
63 | _actors.add(msg.sender);
64 | _;
65 | }
66 |
67 | modifier useActor(uint256 actorIndexSeed) {
68 | currentActor = _actors.rand(actorIndexSeed);
69 | _;
70 | }
71 |
72 | /*//////////////////////////////////////////////////////////////
73 | CONSTRUCTOR
74 | //////////////////////////////////////////////////////////////*/
75 |
76 | constructor(
77 | VertexManager _manager,
78 | address[] memory _spotTokens,
79 | address[] memory _perpTokens,
80 | address _externalAccount
81 | ) {
82 | manager = _manager;
83 | BTC = IERC20Metadata(_perpTokens[0]);
84 | USDC = IERC20Metadata(_perpTokens[1]);
85 | WETH = IERC20Metadata(_perpTokens[2]);
86 |
87 | spotTokens = _spotTokens;
88 | perpTokens = _perpTokens;
89 | externalAccount = _externalAccount;
90 |
91 | fee = manager.getTransactionFee(address(WETH));
92 | }
93 |
94 | /*//////////////////////////////////////////////////////////////
95 | FUNCTIONS
96 | //////////////////////////////////////////////////////////////*/
97 |
98 | function depositPerp(uint256 amountBTC, uint256 amountUSDC, uint256 amountWETH) public createActor {
99 | amountBTC = bound(amountBTC, 0, BTC.balanceOf(address(this)));
100 | amountUSDC = bound(amountUSDC, 0, USDC.balanceOf(address(this)));
101 | amountWETH = bound(amountWETH, 0, WETH.balanceOf(address(this)));
102 |
103 | manager.getTransactionFee(address(BTC)) > amountBTC
104 | ? console.log("pass")
105 | : _depositPerp(perpTokens[0], amountBTC, currentActor);
106 | manager.getTransactionFee(address(USDC)) > amountUSDC
107 | ? console.log("pass")
108 | : _depositPerp(perpTokens[1], amountUSDC, currentActor);
109 | manager.getTransactionFee(address(WETH)) > amountWETH
110 | ? console.log("pass")
111 | : _depositPerp(perpTokens[2], amountWETH, currentActor);
112 | }
113 |
114 | function depositSpot(uint256 amountBTC) public createActor {
115 | amountBTC = bound(amountBTC, 0, BTC.balanceOf(address(this)));
116 |
117 | uint256 amountUSDC = manager.getBalancedAmount(address(BTC), address(USDC), amountBTC);
118 | if (amountUSDC > USDC.balanceOf(address(this))) return;
119 |
120 | _pay(currentActor, BTC, amountBTC);
121 | _pay(currentActor, USDC, amountUSDC);
122 |
123 | vm.deal(currentActor, fee);
124 |
125 | vm.startPrank(currentActor);
126 |
127 | BTC.approve(address(manager), amountBTC);
128 | USDC.approve(address(manager), amountUSDC);
129 |
130 | manager.depositSpot{value: fee}(
131 | 1, spotTokens[0], spotTokens[1], amountBTC, amountUSDC, amountUSDC, currentActor
132 | );
133 |
134 | vm.stopPrank();
135 |
136 | vm.startPrank(externalAccount);
137 | processQueue(manager);
138 | vm.stopPrank();
139 |
140 | ghost_deposits[address(BTC)] += amountBTC;
141 | ghost_deposits[address(USDC)] += amountUSDC;
142 | }
143 |
144 | function withdrawPerp(uint256 actorSeed, uint256 amountBTC, uint256 amountUSDC, uint256 amountWETH)
145 | public
146 | useActor(actorSeed)
147 | {
148 | amountBTC = bound(amountBTC, 0, manager.getUserActiveAmount(2, address(BTC), currentActor));
149 | amountUSDC = bound(amountUSDC, 0, manager.getUserActiveAmount(2, address(USDC), currentActor));
150 | amountWETH = bound(amountWETH, 0, manager.getUserActiveAmount(2, address(WETH), currentActor));
151 |
152 | manager.getTransactionFee(address(BTC)) > amountBTC
153 | ? console.log("pass")
154 | : _withdrawPerp(perpTokens[0], amountBTC, currentActor);
155 | manager.getTransactionFee(address(USDC)) > amountUSDC
156 | ? console.log("pass")
157 | : _withdrawPerp(perpTokens[1], amountUSDC, currentActor);
158 | manager.getTransactionFee(address(WETH)) > amountWETH
159 | ? console.log("pass")
160 | : _withdrawPerp(perpTokens[2], amountWETH, currentActor);
161 | }
162 |
163 | function withdrawSpot(uint256 actorSeed, uint256 amountBTC) public useActor(actorSeed) {
164 | uint256 userActiveAmountBTC = manager.getUserActiveAmount(1, address(BTC), currentActor);
165 | uint256 userActiveAmountUSDC = manager.getUserActiveAmount(1, address(USDC), currentActor);
166 |
167 | amountBTC = bound(amountBTC, 0, userActiveAmountBTC);
168 |
169 | uint256 amountUSDC = manager.getBalancedAmount(address(BTC), address(USDC), amountBTC);
170 | if (amountUSDC > userActiveAmountUSDC) return;
171 |
172 | uint256 feeBTC = manager.getTransactionFee(address(BTC));
173 | if (amountBTC < feeBTC) {
174 | return;
175 | }
176 | ghost_fees[address(BTC)] += feeBTC;
177 |
178 | uint256 feeUSDC = manager.getTransactionFee(address(USDC));
179 | if (amountUSDC < feeUSDC) {
180 | return;
181 | }
182 | ghost_fees[address(USDC)] += feeUSDC;
183 |
184 | vm.deal(currentActor, fee);
185 |
186 | vm.startPrank(currentActor);
187 |
188 | manager.withdrawSpot{value: fee}(1, spotTokens[0], spotTokens[1], amountBTC);
189 |
190 | vm.stopPrank();
191 |
192 | vm.startPrank(externalAccount);
193 | processQueue(manager);
194 | vm.stopPrank();
195 |
196 | ghost_withdraws[address(BTC)] += amountBTC;
197 | ghost_withdraws[address(USDC)] += amountUSDC;
198 | }
199 |
200 | function claimPerp(uint256 actorSeed) public useActor(actorSeed) {
201 | if (currentActor == address(0)) return;
202 | vm.startPrank(currentActor);
203 |
204 | simulate(2, perpTokens, currentActor);
205 |
206 | uint256 beforeBTC = BTC.balanceOf(currentActor);
207 | uint256 beforeUSDC = USDC.balanceOf(currentActor);
208 | uint256 beforeWETH = WETH.balanceOf(currentActor);
209 |
210 | manager.claim(currentActor, perpTokens[0], 2);
211 | manager.claim(currentActor, perpTokens[1], 2);
212 | manager.claim(currentActor, perpTokens[2], 2);
213 |
214 | uint256 receivedBTC = BTC.balanceOf(currentActor) - beforeBTC;
215 | uint256 receivedUSDC = USDC.balanceOf(currentActor) - beforeUSDC;
216 | uint256 receivedWETH = WETH.balanceOf(currentActor) - beforeWETH;
217 |
218 | _pay(address(this), BTC, receivedBTC);
219 | _pay(address(this), USDC, receivedUSDC);
220 | _pay(address(this), WETH, receivedWETH);
221 |
222 | vm.stopPrank();
223 |
224 | ghost_claims[address(BTC)] += receivedBTC;
225 | ghost_claims[address(USDC)] += receivedUSDC;
226 | ghost_claims[address(WETH)] += receivedWETH;
227 | }
228 |
229 | function claimSpot(uint256 actorSeed) public useActor(actorSeed) {
230 | if (currentActor == address(0)) return;
231 | vm.startPrank(currentActor);
232 |
233 | simulate(1, spotTokens, currentActor);
234 |
235 | uint256 beforeBTC = BTC.balanceOf(currentActor);
236 | uint256 beforeUSDC = USDC.balanceOf(currentActor);
237 |
238 | manager.claim(currentActor, spotTokens[0], 1);
239 | manager.claim(currentActor, spotTokens[1], 1);
240 |
241 | uint256 receivedBTC = BTC.balanceOf(currentActor) - beforeBTC;
242 | uint256 receivedUSDC = USDC.balanceOf(currentActor) - beforeUSDC;
243 |
244 | _pay(address(this), BTC, receivedBTC);
245 | _pay(address(this), USDC, receivedUSDC);
246 |
247 | vm.stopPrank();
248 |
249 | ghost_claims[address(BTC)] += receivedBTC;
250 | ghost_claims[address(USDC)] += receivedUSDC;
251 | }
252 |
253 | /*//////////////////////////////////////////////////////////////
254 | HELPERS
255 | //////////////////////////////////////////////////////////////*/
256 |
257 | function _pay(address to, IERC20Metadata token, uint256 amount) internal {
258 | token.transfer(to, amount);
259 | }
260 |
261 | function forEachActor(function(address) external func) public {
262 | return _actors.forEach(func);
263 | }
264 |
265 | function reduceActors(uint256 acc, function(uint256,address) external returns (uint256) func)
266 | public
267 | returns (uint256)
268 | {
269 | return _actors.reduce(acc, func);
270 | }
271 |
272 | function actors() external view returns (address[] memory) {
273 | return _actors.addrs;
274 | }
275 |
276 | function simulate(uint256 id, address[] memory tokens, address user) public {
277 | (address router,,,) = manager.getPoolToken(id, address(0));
278 |
279 | for (uint256 i = 0; i < tokens.length; i++) {
280 | address token = tokens[i];
281 |
282 | deal(token, router, manager.getUserPendingAmount(id, token, user) + manager.getUserFee(id, token, user));
283 | }
284 | }
285 |
286 | function _depositPerp(address token, uint256 amount, address actor) private {
287 | _pay(actor, IERC20Metadata(token), amount);
288 |
289 | vm.deal(actor, fee);
290 |
291 | vm.startPrank(actor);
292 |
293 | IERC20Metadata(token).approve(address(manager), amount);
294 |
295 | manager.depositPerp{value: fee}(2, token, amount, actor);
296 |
297 | vm.stopPrank();
298 |
299 | vm.startPrank(externalAccount);
300 | processQueue(manager);
301 | vm.stopPrank();
302 |
303 | ghost_deposits[token] += amount;
304 | }
305 |
306 | function _withdrawPerp(address token, uint256 amount, address actor) private {
307 | ghost_fees[token] += manager.getTransactionFee(token);
308 |
309 | vm.deal(actor, fee);
310 |
311 | vm.startPrank(actor);
312 |
313 | manager.withdrawPerp{value: fee}(2, token, amount);
314 |
315 | vm.stopPrank();
316 |
317 | vm.startPrank(externalAccount);
318 | processQueue(manager);
319 | vm.stopPrank();
320 |
321 | ghost_withdraws[token] += amount;
322 | }
323 |
324 | // Exclude from coverage report
325 | function test() public {}
326 | }
327 |
--------------------------------------------------------------------------------
/test/utils/AddressSet.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | struct AddressSet {
5 | address[] addrs;
6 | mapping(address => bool) saved;
7 | }
8 |
9 | library LibAddressSet {
10 | function add(AddressSet storage s, address addr) internal {
11 | if (!s.saved[addr]) {
12 | s.addrs.push(addr);
13 | s.saved[addr] = true;
14 | }
15 | }
16 |
17 | function contains(AddressSet storage s, address addr) internal view returns (bool) {
18 | return s.saved[addr];
19 | }
20 |
21 | function count(AddressSet storage s) internal view returns (uint256) {
22 | return s.addrs.length;
23 | }
24 |
25 | function rand(AddressSet storage s, uint256 seed) internal view returns (address) {
26 | if (s.addrs.length > 0) {
27 | return s.addrs[seed % s.addrs.length];
28 | } else {
29 | return address(0);
30 | }
31 | }
32 |
33 | function forEach(AddressSet storage s, function(address) external func) internal {
34 | for (uint256 i; i < s.addrs.length; ++i) {
35 | func(s.addrs[i]);
36 | }
37 | }
38 |
39 | function reduce(AddressSet storage s, uint256 acc, function(uint256,address) external returns (uint256) func)
40 | internal
41 | returns (uint256)
42 | {
43 | for (uint256 i; i < s.addrs.length; ++i) {
44 | acc = func(acc, s.addrs[i]);
45 | }
46 | return acc;
47 | }
48 |
49 | // Exclude from coverage report
50 | function test() public {}
51 | }
52 |
--------------------------------------------------------------------------------
/test/utils/MockToken.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol";
5 |
6 | contract MockToken is ERC20 {
7 | constructor() ERC20("MockToken", "MOCK") {}
8 |
9 | function mint(address a, uint256 b) public {
10 | _mint(a, b);
11 | }
12 |
13 | // Exclude from coverage report
14 | function test() public {}
15 | }
16 |
--------------------------------------------------------------------------------
/test/utils/MockTokenDecimals.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol";
5 |
6 | contract MockTokenDecimals is ERC20 {
7 | uint8 public _decimals;
8 |
9 | constructor(uint8 decimals_) ERC20("MockTokenDecimals", "MOCK") {
10 | _decimals = decimals_;
11 | }
12 |
13 | function mint(address a, uint256 b) public {
14 | _mint(a, b);
15 | }
16 |
17 | function decimals() public view virtual override returns (uint8) {
18 | return _decimals;
19 | }
20 |
21 | // Exclude from coverage report
22 | function test() public {}
23 | }
24 |
--------------------------------------------------------------------------------
/test/utils/ProcessQueue.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import {IEndpoint} from "src/interfaces/IEndpoint.sol";
7 | import {VertexManager, IVertexManager} from "src/VertexManager.sol";
8 |
9 | contract ProcessQueue is Test {
10 | /// @notice Processes any transactions in the Elixir queue.
11 | function processQueue(VertexManager manager) internal {
12 | // Loop through the queue and process each transaction using the idTo provided.
13 | for (uint128 i = manager.queueUpTo() + 1; i < manager.queueCount() + 1; i++) {
14 | VertexManager.Spot memory spot = manager.nextSpot();
15 |
16 | if (spot.spotType == IVertexManager.SpotType.DepositSpot) {
17 | IVertexManager.DepositSpot memory spotTxn = abi.decode(spot.transaction, (IVertexManager.DepositSpot));
18 |
19 | uint256 amount1 = manager.getBalancedAmount(spotTxn.token0, spotTxn.token1, spotTxn.amount0);
20 |
21 | manager.unqueue(
22 | i,
23 | abi.encode(
24 | IVertexManager.DepositSpotResponse({
25 | amount1: amount1,
26 | token0Shares: spotTxn.amount0,
27 | token1Shares: amount1
28 | })
29 | )
30 | );
31 | } else if (spot.spotType == IVertexManager.SpotType.DepositPerp) {
32 | IVertexManager.DepositPerp memory spotTxn = abi.decode(spot.transaction, (IVertexManager.DepositPerp));
33 |
34 | manager.unqueue(i, abi.encode(IVertexManager.DepositPerpResponse({shares: spotTxn.amount})));
35 | } else if (spot.spotType == IVertexManager.SpotType.WithdrawPerp) {
36 | IVertexManager.WithdrawPerp memory spotTxn = abi.decode(spot.transaction, (IVertexManager.WithdrawPerp));
37 |
38 | manager.unqueue(i, abi.encode(IVertexManager.WithdrawPerpResponse({amountToReceive: spotTxn.amount})));
39 | } else if (spot.spotType == IVertexManager.SpotType.WithdrawSpot) {
40 | IVertexManager.WithdrawSpot memory spotTxn = abi.decode(spot.transaction, (IVertexManager.WithdrawSpot));
41 |
42 | uint256 amount1 = manager.getBalancedAmount(spotTxn.token0, spotTxn.token1, spotTxn.amount0);
43 |
44 | manager.unqueue(
45 | i,
46 | abi.encode(
47 | IVertexManager.WithdrawSpotResponse({
48 | amount1: amount1,
49 | amount0ToReceive: spotTxn.amount0,
50 | amount1ToReceive: amount1
51 | })
52 | )
53 | );
54 | } else {}
55 | }
56 | }
57 |
58 | /// @notice Processes any transactions in the Vertex queue.
59 | function processSlowModeTxs(IEndpoint endpoint) internal {
60 | // Clear any external slow-mode txs from the Vertex queue.
61 | vm.warp(block.timestamp + 259200);
62 | (, uint64 txUpTo, uint64 txCount) = endpoint.getSlowModeTx(0);
63 |
64 | // Loop over remaining queue.
65 | for (uint256 i = txUpTo; i < txCount; i++) {
66 | endpoint.executeSlowModeTransaction();
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/test/utils/Utils.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BUSL-1.1
2 | pragma solidity 0.8.18;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | contract Utils is Test {
7 | bytes32 internal nextUser = keccak256(abi.encodePacked("user address"));
8 |
9 | function getNextUserAddress() external returns (address payable) {
10 | address payable user = payable(address(uint160(uint256(nextUser))));
11 | nextUser = keccak256(abi.encodePacked(nextUser));
12 | return user;
13 | }
14 |
15 | // create users with 100 ETH balance each
16 | function createUsers(uint256 userNum) external returns (address payable[] memory) {
17 | address payable[] memory users = new address payable[](userNum);
18 | for (uint256 i = 0; i < userNum; i++) {
19 | address payable user = this.getNextUserAddress();
20 | vm.deal(user, 100 ether);
21 | users[i] = user;
22 | }
23 |
24 | return users;
25 | }
26 |
27 | // move block.number forward by a given number of blocks
28 | function mineBlocks(uint256 numBlocks) external {
29 | uint256 targetBlock = block.number + numBlocks;
30 | vm.roll(targetBlock);
31 | }
32 |
33 | // Exclude from coverage report
34 | function test() public {}
35 | }
36 |
--------------------------------------------------------------------------------