Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 64 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Valantis Stake Exchange AMM (STEX AMM)

STEX AMM is a novel AMM uniquely designed for redeemable assets such as Liquid Staking Tokens (LSTs), designed in collaboration with [Thunderhead](https://thunderhead.xyz/).
STEX AMM is a novel AMM uniquely designed for yield-bearing assets which are redeemable for their underlying, such as Liquid Staking Tokens (LSTs), designed in collaboration with [Thunderhead](https://thunderhead.xyz/).

LSTs are backed 1:1 by an equivalent amount of native asset (in the absence of slashing). However, generic AMMs fail to account for this simple fact, forcing LPs to sell the LST for less than fair value, resulting in significant cumulative losses to arbitrageurs. Moreover, there are times where excess liquidity can be put to earn extra yield on external protocols (e.g. lending markets like AAVE and Euler) and only be brought back into the AMM if needed to absorb incoming swap volume.

Expand All @@ -17,19 +17,33 @@ STEX AMM solves these two structural inefficiencies by integrating with the LST'

Contains STEX AMM's core contracts, Module dependencies and Mock contracts used for testing.

**AaveLendingModule.sol**: This is a dedicated module, compatible with AAVE V3's `supply` and `withdraw` functions. Its owner can deposit and withdraw a portion of Wrapped Native Token's pool reserves, with the goal of optimizing overall yield.
**AaveLendingModule.sol**: Dedicated Lending Module compatible with AAVE V3's `supply` and `withdraw` functions. Its owner can deposit and withdraw a portion of pool's `token1` reserves, with the goal of optimizing overall yield.

**ERC4626LendingModule.sol**: Dedicated Lending Module compatible with ERC4626 standard vaults. Its owner can deposit and withdraw a portion of pool's `token1` reserves, with the goal of optimizing overall yield.

**MultiMarketLendingModule.sol**: Lending Module which can deposit and withdraw an asset across multiple Lending Modules, such as `AaveLendingModule` and `ERC4626LendingModule`.

**STEXAMM.sol**: The main contract which implements the core mechanisms of STEX AMM, built as a Valantis [Liquidity Module](https://docs.valantis.xyz/sovereign-pool-subpages/modules/liquidity-module).

**STEXLens.sol**: Helper contract that contains read-only functions which are useful to simulate state updates in `STEXAMM`.
**STEXLens.sol**: Helper contract that contains read-only functions which are useful to simulate state updates in `STEXAMM`. WARNING: Only compatible with `stHYPEWithdrawalModule` and `kHYPEWithdrawalModule`.

**STEXRatioSwapFeeModule.sol**: Contains a dynamic fee mechanism for swaps on STEX AMM, based on the ratio of reserves between the LST and wrapped native token, built as a Valantis [Swap Fee Module](https://docs.valantis.xyz/sovereign-pool-subpages/modules/swap-fee-module).

**STEXRatioSwapFeeModule.sol**: Contains a dynamic fee mechanism for swaps on STEX AMM, built as a Valantis [Swap Fee Module](https://docs.valantis.xyz/sovereign-pool-subpages/modules/swap-fee-module).
**StepwiseFeeModule.sol**: Contains a dynamic fee mechanism for swaps on STEX AMM, using a stepwise function pricing curve whose shape can be determined off-chain using sophisticated models and pricing information, built as a Valantis [Swap Fee Module](https://docs.valantis.xyz/sovereign-pool-subpages/modules/swap-fee-module).

**stHYPEWithdrawalModule.sol**: Module that manages all of STEX AMM's interactions with [stakedHYPE](https://www.stakedhype.fi/), a leading LST protocol on HyperEVM developed by [Thunderhead](https://thunderhead.xyz/).

**owner/WithdrawalModuleManager.sol\***: Custom contract that has the `owner` role in `stHYPEWithdrawalModule`. It is controlled by a multi-sig.
**kHYPEWithdrawalModule.sol**: Module that manages all of STEX AMM's interactions with [kHYPE](https://kinetiq.xyz/), a leading LST protocol on HyperEVM developed by [Kinetiq](https://kinetiq.xyz/).

**owner/stHYPEWithdrawalModuleManager.sol**: Custom contract that has the `owner` role in `stHYPEWithdrawalModule`. It is controlled by a multi-sig.

**owner/stHYPEWithdrawalModuleKeeper.sol**: A sub-role in `stHYPEWithdrawalModuleManager`, executing smart contract calls that require automation but which are not mission critical.

**owner/kHYPEWithdrawalModuleManager.sol**: Custom contract that has the `owner` role in `kHYPEWithdrawalModule`. It is controlled by a multi-sig.

**owner/WithdrawalModuleKeeper.sol**: A sub-role in `WithdrawalModuleManager`, executing smart contract calls that require automation but which are not mission critical.
**owner/kHYPEWithdrawalModuleKeeper.sol**: A sub-role in `kHYPEWithdrawalModuleManager`, executing smart contract calls that require automation but which are not mission critical.

**owner/StepwiseFeeModuleKeeper.sol**: A sub-role in `StepwiseFeeModule`, setting and updating dynamic fee parameters.

**interfaces/**: Contains all relevant interfaces.

Expand Down Expand Up @@ -71,6 +85,50 @@ Copy .env.example file to .env and set the variables.

**Note:** All deployment bash scripts are, by default, in simulation mode. Add `--broadcast` flag to trigger deployments, and `--verify` for block explorer contract verification.

### Risks and Trust Assumptions: kHYPEWithdrawalModule

**Owner role in kHYPEWithdrawalModule**

Role:

- Can update the `Lending Module` under 3-7 day timelock.
- Can send any amount of token1 to the `Lending Module`'s deposit function.
- Can withdraw any amount of token1 from the `Lending Module`'s withdraw function and send it to `pool`.
- Can unstake the `pool`'s entire token0 balance to the kHYPE `StakingManager` queueWithdrawal function, queuing for withdrawal with `kHYPEWithdrawalModule` being the recipient.
- Can stake the `pool`'s entire token1 balance to the kHYPE `StakingManager` stake function, with the pool being the recipient.
- Can atomically rebalance the `pool`'s token0 reserves into token1, as long as the cost of rebalance does not exceed the fee which would be paid by unstaking through `StakingManager`
- Can use liquid token1 reserves in the `pool` to net off against pending LP withdrawals, delivering faster LP withdrawals whenever available.
- Can rescue any locked tokens which are not the native token, token0 nor token1.
- Can unstake any surplus balance of token0 to the kHYPE `StakingManager` queueWithdrawal function. This can happen in case of donations or cancelled unstaking operations by `StakingManager`.

Risks:

- `owner` can steal token1 reserves with a minimum 3-day timelock by upgrading the `Lending Module` to a malicious contract. Currently the `owner` role is managed by a trusted multi-sig.
- Due to the fact that Kinetiq protocol can update unstaking fees, and unstaking of token0 reserves is managed by `owner` and is separate from the LP's withdrawal initiation, there can be a mismatch between the amount of HYPE which the LP expects to receive vs. what is actually returned after unstaking is processed. `owner` is trusted to quickly unstake after LPs initiate withdrawal requests and to pay attention to any sudden changes in Kinetiq's unstaking fee, in order to minimize such discrepancies.

**Lending Module in kHYPEWithdrawalModule**

The integrated lending protocol in `Lending Module` custodies a portion of token1 reserves determined by `owner`.
For example, `AAVELendingModule` contract provides a concrete implementation compatible with AAVE V3 deployments.

Risks:

- If the integrated lending protocol becomes insolvent or contains faulty withdrawal logic, funds will be lost.
- If the integrated lending protocol is working correctly but does not have enough liquidity to honor instant withdrawals, then LP withdrawals via `STEX AMM` withdraw function would become temporarily blocked. In this scenario, the user is expected to withdraw at a later stage.
- It is assumed that the lending protocol's deposit function does not allow for partially deposited amounts. If that is the case, `Lending Module` would need to handle refunds of unused token amounts back into the pool.

**Kinetiq Protocol Risks and Assumptions:** kHYPE AMM integrates with the `StakingManager` contract for token0 (kHYPE) withdrawals, and `StakingAccountant` for reading exchange rates between kHYPE and HYPE. kHYPE AMM allows the external custody of up to 100% of the AMM's token0 reserves as Pending Withdrawals. `StakingManager` and `Staking Accountant` are upgradable, and kHYPE AMM trusts the management of Kinetiq protocol to manage its kHYPE assets for delayed redemption at its true rate.

This codebase is currently intended to deployed on Hyperliquid's HyperEVM, where slashing is not active. This is why the kHYPEWithdrawalModule contract always assumes that `StakingManager` will honor kHYPE withdrawal requests amount at 1:1 rate against Native Token, excluding the fee paid in kHYPE at the time where unstaking is initiated.

**Slashing Risk:**

kHYPE AMM protects against secondary-market depeg arbitrage loss by never selling kHYPE below the true peg (determined by `StakingAccountant` contract). kHYPE reserves in the AMM are exposed to real slashing events in the same way as holding kHYPE directly. The risk is only based on the current kHYPE reserves of the pool at the time of slashing, and does not create further arbitrage loss. Currently, Hyperliquid does not have slashing enabled.

**Inventory Risk**

An LP position can consist of HYPE, kHYPE, and pending kHYPE withdrawals. Upon withdrawing the LP position, a user may withdraw their illiquid portion of pending kHYPE Withdrawals instantly for a fee. In the case of pending withdrawals, to be given the full value of their position, the user must wait until maturity of their pending withdrawal.

### License

Stake Exchange AMM is licensed under the Business Source License 1.1 (BUSL-1.1), see [BUSL_LICENSE](licenses/BUSL_LICENSE), and the MIT Licence (MIT), see [MIT_LICENSE](licenses/MIT_LICENSE). Each file in Stake Exhange AMM states the applicable license type in the header.
Binary file added audits/zenith-aug-25-lending-modules.pdf
Binary file not shown.
1 change: 1 addition & 0 deletions deploy_khype_mocks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eval $(grep '^RPC_URL' .env) && forge script scripts/kHYPEMocksDeploy.s.sol:kHYPEMocksDeployScript --rpc-url $RPC_URL
1 change: 1 addition & 0 deletions deploy_khype_multimarket_lending_module.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eval $(grep '^RPC_URL' .env) && forge script scripts/kHYPESTEXMultiMarketLendingDeploy.s.sol:kHYPESTEXMultiMarketLendingDeployScript --rpc-url $RPC_URL
1 change: 1 addition & 0 deletions deploy_khype_rebalance_module.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eval $(grep '^RPC_URL' .env) && forge script scripts/kHYPERebalanceModuleDeploy.s.sol:kHYPERebalanceModuleDeployScript --rpc-url $RPC_URL
1 change: 1 addition & 0 deletions deploy_khype_stex.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eval $(grep '^RPC_URL' .env) && forge script scripts/kHYPESTEXDeploy.s.sol:kHYPESTEXDeployScript --rpc-url $RPC_URL
1 change: 1 addition & 0 deletions deploy_stepwise_fee_module.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eval $(grep '^RPC_URL' .env) && forge script scripts/StepwiseFeeModuleDeploy.s.sol:StepwiseFeeModuleDeployScript --rpc-url $RPC_URL
1 change: 0 additions & 1 deletion deploy_stex.sh

This file was deleted.

1 change: 1 addition & 0 deletions deploy_sthype_stex.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eval $(grep '^RPC_URL' .env) && forge script scripts/stHYPESTEXDeploy.s.sol:stHYPESTEXDeployScript --rpc-url $RPC_URL
1 change: 1 addition & 0 deletions khype_propose_lending_module.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eval $(grep '^RPC_URL' .env) && forge script scripts/kHYPESTEXLendingModuleProposal.s.sol:kHYPESTEXLendingModuleProposalScript --rpc-url $RPC_URL
1 change: 1 addition & 0 deletions khype_stex_lp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eval $(grep '^RPC_URL' .env) && forge script scripts/kHYPESTEXLP.s.sol:kHYPESTEXLPScript --rpc-url $RPC_URL
1 change: 1 addition & 0 deletions khype_stex_swap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eval $(grep '^RPC_URL' .env) && forge script scripts/kHYPESTEXSwap.s.sol:kHYPESTEXSwapScript --rpc-url $RPC_URL
13 changes: 7 additions & 6 deletions scripts/LendingModuleProposal.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {Test} from "forge-std/Test.sol";

import {ISovereignPool} from "@valantis-core/pools/interfaces/ISovereignPool.sol";

import {AaveLendingModule} from "src/AaveLendingModule.sol";
import {AaveLendingModule} from "src/lending-modules/AaveLendingModule.sol";
import {MultiMarketLendingModule} from "src/lending-modules/MultiMarketLendingModule.sol";
import {STEXAMM} from "src/STEXAMM.sol";
import {stHYPEWithdrawalModule} from "src/stHYPEWithdrawalModule.sol";
import {WithdrawalModuleKeeper} from "src/owner/WithdrawalModuleKeeper.sol";
import {WithdrawalModuleManager} from "src/owner/WithdrawalModuleManager.sol";
import {stHYPEWithdrawalModule} from "src/withdrawal-modules/stHYPEWithdrawalModule.sol";
import {stHYPEWithdrawalModuleKeeper} from "src/owner/stHYPEWithdrawalModuleKeeper.sol";
import {stHYPEWithdrawalModuleManager} from "src/owner/stHYPEWithdrawalModuleManager.sol";

contract LendingModuleProposalScript is Script, Test {
function run() external {
Expand Down Expand Up @@ -41,12 +42,12 @@ contract LendingModuleProposalScript is Script, Test {

console.log("STEX AMM: ", address(stex));

WithdrawalModuleKeeper keeper = WithdrawalModuleKeeper(
stHYPEWithdrawalModuleKeeper keeper = stHYPEWithdrawalModuleKeeper(
0x0Aef1eAAd539C16292faEB16D3F4AB5842F0aa6c
);
assertEq(keeper.owner(), ownerMultisig);

WithdrawalModuleManager manager = WithdrawalModuleManager(
stHYPEWithdrawalModuleManager manager = stHYPEWithdrawalModuleManager(
0x80c7f89398160fCD9E74519f63F437459E5d02E2
);
assertEq(manager.owner(), ownerMultisig);
Expand Down
2 changes: 1 addition & 1 deletion scripts/OverseerInteractions.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.25;
import "forge-std/Script.sol";
import {Test} from "forge-std/Test.sol";

import {IOverseer} from "src/interfaces/IOverseer.sol";
import {IOverseer} from "src/interfaces/sthype/IOverseer.sol";
import {IWithdrawalModule} from "src/interfaces/IWithdrawalModule.sol";
import {LPWithdrawalRequest} from "src/structs/WithdrawalModuleStructs.sol";

Expand Down
2 changes: 1 addition & 1 deletion scripts/STEXLP.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ contract STEXLPScript is Script, Test {
uint256 shares = stexLens.getSharesForDeposit(address(stex), amount);

DepositWrapper depositWrapper = DepositWrapper(
payable(0x644195021278674bd8F7574e17018d32d8E75A98)
payable(0x640b752B6452C7FeE6afE15e0667EBeB058aB0D2)
);
uint256 sharesDeposited = depositWrapper.depositFromNative{
value: amount
Expand Down
1 change: 0 additions & 1 deletion scripts/STEXLensDeploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import "forge-std/Script.sol";
import {Test} from "forge-std/Test.sol";

import {STEXLens} from "src/STEXLens.sol";
import {stHYPEWithdrawalModule} from "src/stHYPEWithdrawalModule.sol";

contract STEXLensDeployScript is Script, Test {
function run() external {
Expand Down
4 changes: 2 additions & 2 deletions scripts/STEXSwap.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ contract STEXSwapScript is Script, Test {
vm.startBroadcast(deployerPrivateKey);

STEXAMM stex = STEXAMM(
payable(0x39694eFF3b02248929120c73F90347013Aec834d)
payable(0xd43d4444fc88E1fC205AA303DBd52aCf3493Fd02)
);

address token0 = stex.token0();
address token1 = stex.token1();

address tokenIn = token0;
uint256 amount = 0.01 ether;
uint256 amount = 0.005 ether;

uint256 amountOut = stex.getAmountOut(tokenIn, amount, false);

Expand Down
125 changes: 125 additions & 0 deletions scripts/StepwiseFeeModuleDeploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import "forge-std/Script.sol";
import {Test} from "forge-std/Test.sol";

import {STEXAMM} from "src/STEXAMM.sol";
import {StepwiseFeeModule} from "src/swap-fee-modules/StepwiseFeeModule.sol";
import {StepwiseFeeModuleKeeper} from "src/owner/StepwiseFeeModuleKeeper.sol";

contract StepwiseFeeModuleDeployScript is Script, Test {
function run() external {
uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
address deployerAddress = vm.addr(deployerPrivateKey);

if (block.chainid != 999) revert("Chain ID not Hyper EVM mainnet");

console.log("Deployer address: ", deployerAddress);

vm.startBroadcast(deployerPrivateKey);

// Address of owner multi-sig wallet
address ownerMultisig = 0xe26dA5cBf101bDA4028E2B3208c32424f5D09421;

STEXAMM stex = STEXAMM(
payable(0xbf747D2959F03332dbd25249dB6f00F62c6Cb526)
);

// Uncomment to deploy Swap Fee Module
/*StepwiseFeeModule swapFeeModule = new StepwiseFeeModule(
deployerAddress
);
assertEq(swapFeeModule.owner(), deployerAddress);*/
StepwiseFeeModule swapFeeModule = StepwiseFeeModule(
0x14EFe613b8a1fce7142286D0bC70723519bc4485
);

// Uncomment to deploy Keeper
/*StepwiseFeeModuleKeeper keeper = new StepwiseFeeModuleKeeper(
deployerAddress
);*/
StepwiseFeeModuleKeeper keeper = StepwiseFeeModuleKeeper(
0x623cD8e6CA491A30BeE10Cb91034B059a162C007
);

// Uncomment to whitelist a keeper address
address keeperEOA = 0x35826836418Ece9D00e4436bb91CA9a9d5136b30;
//keeper.setKeeper(keeperEOA);
assertTrue(keeper.isKeeper(keeperEOA));

// Uncomment to remove a keeper address
//keeper.removeKeeper(deployerAddress);
//assertFalse(keeper.isKeeper(deployerAddress));

// Uncomment to transfer ownership of Keeper
//keeper.transferOwnership(ownerMultisig);
assertEq(keeper.owner(), ownerMultisig);

// Uncomment to generate payload to propose Swap Fee Module update under timelock
/*vm.startPrank(ownerMultisig);
bytes memory swapFeeModuleProposalPayload = abi.encodeWithSelector(
STEXAMM.proposeSwapFeeModule.selector,
address(swapFeeModule),
3 days
);
console.log("payload for swap fee module proposal: ");
console.logBytes(swapFeeModuleProposalPayload);
(bool success, ) = address(stex).call(swapFeeModuleProposalPayload);
assertTrue(success);
(address swapFeeModuleProposed, uint256 startTimestamp) = stex
.swapFeeModuleProposal();
assertEq(swapFeeModuleProposed, address(swapFeeModule));
assertEq(startTimestamp, block.timestamp + 3 days);

vm.warp(block.timestamp + 3 days);

stex.setProposedSwapFeeModule();

vm.stopPrank();*/

// Uncomment to transfer ownership of Swap Fee Module
//swapFeeModule.transferOwnership(address(keeper));
assertEq(swapFeeModule.owner(), address(keeper));

// Uncomment to set Swap Fee Module params
/*{
uint256 minThresholdToken1 = 10_000 ether;
uint256 maxThresholdToken1 = 200_000 ether;
uint256 numSteps = 5;
uint32[] memory feeStepsInBipsToken0 = new uint32[](numSteps);
feeStepsInBipsToken0[0] = 3;
feeStepsInBipsToken0[1] = 20;
feeStepsInBipsToken0[2] = 50;
feeStepsInBipsToken0[3] = 200;
feeStepsInBipsToken0[4] = 500;

keeper.setFeeParamsToken0(
address(swapFeeModule),
minThresholdToken1,
maxThresholdToken1,
feeStepsInBipsToken0
);
}*/
/*{
uint256 minThresholdToken1 = swapFeeModule.minThresholdToken1();
uint256 maxThresholdToken1 = swapFeeModule.maxThresholdToken1();
uint32[] memory feeStepsInBips = swapFeeModule.getToken0FeeInBips();

assertEq(minThresholdToken1, 10_000 ether);
assertEq(maxThresholdToken1, 200_000 ether);
assertEq(feeStepsInBips[1], 20);
assertEq(feeStepsInBips[2], 50);
assertEq(feeStepsInBips[3], 200);
assertEq(feeStepsInBips[4], 500);
assertEq(swapFeeModule.numStepsToken0FeeCurve(), 5);
}*/

// Uncomment to set STEX AMM's pool in Swap Fee Module
address pool = stex.pool();
//swapFeeModule.setPool(pool);
assertEq(swapFeeModule.pool(), pool);

vm.stopBroadcast();
}
}
Loading