Skip to content

Commit

Permalink
Added a slippage guard to addLiquidity on the realized LP share pri…
Browse files Browse the repository at this point in the history
…ce (#728)

* Added a slippage guard to `addLiquidity` on the realized LP share price

* Fixed the rust agent
  • Loading branch information
jalextowle authored Jan 18, 2024
1 parent afdc3fd commit 6b3cb74
Show file tree
Hide file tree
Showing 18 changed files with 107 additions and 45 deletions.
1 change: 1 addition & 0 deletions contracts/src/external/Hyperdrive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ abstract contract Hyperdrive is
uint256,
uint256,
uint256,
uint256,
IHyperdrive.Options calldata
) external payable returns (uint256) {
_delegate(target1);
Expand Down
15 changes: 14 additions & 1 deletion contracts/src/external/HyperdriveTarget1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,30 @@ abstract contract HyperdriveTarget1 is

/// @notice Allows LPs to supply liquidity for LP shares.
/// @param _contribution The amount of base to supply.
/// @param _minLpSharePrice The minimum LP share price the LP is willing
/// to accept for their shares. LP's incur negative slippage when
/// adding liquidity if there is a net curve position in the market,
/// so this allows LPs to protect themselves from high levels of
/// slippage.
/// @param _minApr The minimum APR at which the LP is willing to supply.
/// @param _maxApr The maximum APR at which the LP is willing to supply.
/// @param _options The options that configure how the operation is settled.
/// @return lpShares The number of LP tokens created.
function addLiquidity(
uint256 _contribution,
uint256 _minLpSharePrice,
uint256 _minApr,
uint256 _maxApr,
IHyperdrive.Options calldata _options
) external payable returns (uint256 lpShares) {
return _addLiquidity(_contribution, _minApr, _maxApr, _options);
return
_addLiquidity(
_contribution,
_minLpSharePrice,
_minApr,
_maxApr,
_options
);
}

/// @notice Allows an LP to burn shares and withdraw from the pool.
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/instances/erc4626/ERC4626Hyperdrive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ contract ERC4626Hyperdrive is Hyperdrive, ERC4626Base {
// deposit then this line will read an incorrect and possibly dangerous
// price.
if (_config.initialVaultSharePrice != _pricePerVaultShare()) {
revert IHyperdrive.InvalidInitialSharePrice();
revert IHyperdrive.InvalidInitialVaultSharePrice();
}

// Ensure that the base token is the same as the vault's underlying
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/instances/steth/StETHHyperdrive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ contract StETHHyperdrive is Hyperdrive, StETHBase {

// Ensure that the initial vault share price is properly configured.
if (_config.initialVaultSharePrice != _pricePerVaultShare()) {
revert IHyperdrive.InvalidInitialSharePrice();
revert IHyperdrive.InvalidInitialVaultSharePrice();
}
}

Expand Down
3 changes: 2 additions & 1 deletion contracts/src/interfaces/IHyperdrive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,8 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveCore, IMultiToken {
error InvalidFeeAmounts();
error InvalidFeeDestination();
error InvalidIndexes();
error InvalidInitialSharePrice();
error InvalidInitialVaultSharePrice();
error InvalidLpSharePrice();
error InvalidMaturityTime();
error InvalidMinimumShareReserves();
error InvalidPositionDuration();
Expand Down
1 change: 1 addition & 0 deletions contracts/src/interfaces/IHyperdriveCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface IHyperdriveCore is IMultiTokenCore {

function addLiquidity(
uint256 _contribution,
uint256 _minLpSharePrice,
uint256 _minApr,
uint256 _maxApr,
IHyperdrive.Options calldata _options
Expand Down
11 changes: 11 additions & 0 deletions contracts/src/internal/HyperdriveLP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,18 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken {

/// @dev Allows LPs to supply liquidity for LP shares.
/// @param _contribution The amount to supply.
/// @param _minLpSharePrice The minimum LP share price the LP is willing
/// to accept for their shares. LP's incur negative slippage when
/// adding liquidity if there is a net curve position in the market,
/// so this allows LPs to protect themselves from high levels of
/// slippage.
/// @param _minApr The minimum APR at which the LP is willing to supply.
/// @param _maxApr The maximum APR at which the LP is willing to supply.
/// @param _options The options that configure how the operation is settled.
/// @return lpShares The number of LP tokens created
function _addLiquidity(
uint256 _contribution,
uint256 _minLpSharePrice,
uint256 _minApr,
uint256 _maxApr,
IHyperdrive.Options calldata _options
Expand Down Expand Up @@ -195,6 +201,11 @@ abstract contract HyperdriveLP is HyperdriveBase, HyperdriveMultiToken {
}
}

// Enforce the minimum LP share price slippage guard.
if (_contribution.divDown(lpShares) < _minLpSharePrice) {
revert IHyperdrive.InvalidLpSharePrice();
}

// Mint LP shares to the supplier.
_mint(AssetId._LP_ASSET_ID, _options.destination, lpShares);

Expand Down
1 change: 1 addition & 0 deletions crates/test-utils/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ impl Agent<ChainClient, ChaCha8Rng> {
let tx = ContractCall_(self.hyperdrive.add_liquidity(
contribution.into(),
uint256!(0),
uint256!(0),
U256::MAX,
Options {
destination: self.address,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ contract NegativeInterestLongFeeTest is HyperdriveTest {
DepositOverrides({
asBase: true,
depositAmount: basePaid,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0, // TODO: This should never go below the base amount. Investigate this.
maxSlippage: type(uint256).max,
extraData: new bytes(0)
Expand Down Expand Up @@ -354,7 +354,7 @@ contract NegativeInterestLongFeeTest is HyperdriveTest {
DepositOverrides({
asBase: true,
depositAmount: basePaid,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0, // TODO: This should never go below the base amount. Investigate this.
maxSlippage: type(uint256).max,
extraData: new bytes(0)
Expand Down Expand Up @@ -559,7 +559,7 @@ contract NegativeInterestLongFeeTest is HyperdriveTest {
DepositOverrides({
asBase: true,
depositAmount: basePaid,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0, // TODO: This should never go below the base amount. Investigate this.
maxSlippage: type(uint256).max,
extraData: new bytes(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ contract NegativeInterestShortFeeTest is HyperdriveTest {
asBase: true,
// NOTE: Roughly double deposit amount needed to cover 100% flat fee
depositAmount: shortAmount * 2,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint256).max,
extraData: new bytes(0)
Expand Down Expand Up @@ -559,7 +559,7 @@ contract NegativeInterestShortFeeTest is HyperdriveTest {
asBase: true,
// NOTE: Roughly double deposit amount needed to cover 100% flat fee
depositAmount: shortAmount * 2,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint256).max,
extraData: new bytes(0)
Expand Down
11 changes: 7 additions & 4 deletions test/integrations/hyperdrive/NonstandardDecimals.sol
Original file line number Diff line number Diff line change
Expand Up @@ -511,13 +511,15 @@ contract NonstandardDecimalsTest is HyperdriveTest {
);

// Celine adds liquidity.
// Note that fuzzing will occasionally create long and short trades so large
// that the Celine gets less than the minimum transaction amount. This situation
// can be caught by setting Min/Max APR slippage guards and checking for revert
//
// NOTE: Fuzzing will occasionally create long and short trades so large
// that the Celine gets less than the minimum transaction amount. This
// situation can be caught by setting Min/Max APR slippage guards and
// checking for reverts.
DepositOverrides memory overrides = DepositOverrides({
asBase: true,
depositAmount: testParams.contribution,
minVaultSharePrice: 0, // unused
minSharePrice: 0, // unused
minSlippage: spotAPRBefore - 0.015e18, // min spot rate of .5%
maxSlippage: spotAPRBefore + 0.015e18, // max spot rate of 3.5%
extraData: new bytes(0) // unused
Expand All @@ -533,6 +535,7 @@ contract NonstandardDecimalsTest is HyperdriveTest {

celineLpShares = hyperdrive.addLiquidity(
overrides.depositAmount,
overrides.minSharePrice, // min share price
overrides.minSlippage, // min spot rate
overrides.maxSlippage, // max spot rate
IHyperdrive.Options({
Expand Down
11 changes: 6 additions & 5 deletions test/integrations/hyperdrive/ReentrancyTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ contract ReentrancyTest is HyperdriveTest {
(
CONTRIBUTION,
0,
0,
1e18,
IHyperdrive.Options({
destination: _trader,
Expand Down Expand Up @@ -289,7 +290,7 @@ contract ReentrancyTest is HyperdriveTest {
DepositOverrides({
asBase: true,
depositAmount: CONTRIBUTION + 1,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint256).max,
extraData: new bytes(0)
Expand Down Expand Up @@ -318,7 +319,7 @@ contract ReentrancyTest is HyperdriveTest {
DepositOverrides({
asBase: true,
depositAmount: CONTRIBUTION + 1,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint256).max,
extraData: new bytes(0)
Expand Down Expand Up @@ -385,7 +386,7 @@ contract ReentrancyTest is HyperdriveTest {
DepositOverrides({
asBase: true,
depositAmount: BASE_PAID + 1,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint256).max,
extraData: new bytes(0)
Expand Down Expand Up @@ -426,7 +427,7 @@ contract ReentrancyTest is HyperdriveTest {
asBase: true,
// NOTE: Roughly double deposit amount needed to cover 100% flat fee
depositAmount: BOND_AMOUNT * 2,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint256).max,
extraData: new bytes(0)
Expand All @@ -445,7 +446,7 @@ contract ReentrancyTest is HyperdriveTest {
asBase: true,
// NOTE: Roughly double deposit amount needed to cover 100% flat fee
depositAmount: BOND_AMOUNT * 2,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint256).max,
extraData: new bytes(0)
Expand Down
30 changes: 29 additions & 1 deletion test/units/hyperdrive/AddLiquidityTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ contract AddLiquidityTest is HyperdriveTest {
vm.startPrank(bob);
vm.expectRevert(IHyperdrive.MinimumTransactionAmount.selector);
hyperdrive.addLiquidity(
0,
0,
0,
type(uint256).max,
Expand All @@ -56,6 +57,7 @@ contract AddLiquidityTest is HyperdriveTest {
vm.startPrank(bob);
vm.expectRevert(IHyperdrive.NotPayable.selector);
hyperdrive.addLiquidity{ value: 1 }(
0,
0,
0,
type(uint256).max,
Expand All @@ -80,6 +82,7 @@ contract AddLiquidityTest is HyperdriveTest {
vm.startPrank(bob);
vm.expectRevert(IHyperdrive.Paused.selector);
hyperdrive.addLiquidity(
0,
0,
0,
type(uint256).max,
Expand Down Expand Up @@ -111,6 +114,7 @@ contract AddLiquidityTest is HyperdriveTest {
hyperdrive.addLiquidity(
1e18,
0,
0,
type(uint256).max,
IHyperdrive.Options({
destination: bob,
Expand All @@ -120,7 +124,7 @@ contract AddLiquidityTest is HyperdriveTest {
);
}

function test_add_liquidity_failure_invalid_apr() external {
function test_add_liquidity_failure_slippage_guards() external {
uint256 apr = 0.05e18;

// Initialize the pool with a large amount of capital.
Expand All @@ -133,6 +137,7 @@ contract AddLiquidityTest is HyperdriveTest {
vm.expectRevert(IHyperdrive.InvalidApr.selector);
hyperdrive.addLiquidity(
10e18,
0,
0.06e18,
type(uint256).max,
IHyperdrive.Options({
Expand All @@ -149,13 +154,34 @@ contract AddLiquidityTest is HyperdriveTest {
hyperdrive.addLiquidity(
10e18,
0,
0,
0.04e18,
IHyperdrive.Options({
destination: bob,
asBase: true,
extraData: new bytes(0)
})
);

// Attempt to add liquidity with a minimum LP share price that is too
// high.
vm.stopPrank();
vm.startPrank(bob);
uint256 lpSharePrice = hyperdrive.getPoolInfo().lpSharePrice;
baseToken.mint(10e18);
baseToken.approve(address(hyperdrive), 10e18);
vm.expectRevert(IHyperdrive.InvalidLpSharePrice.selector);
hyperdrive.addLiquidity(
10e18,
2 * lpSharePrice,
0,
type(uint256).max,
IHyperdrive.Options({
destination: bob,
asBase: true,
extraData: new bytes(0)
})
);
}

function test_add_liquidity_failure_zero_lp_total_supply() external {
Expand Down Expand Up @@ -183,6 +209,7 @@ contract AddLiquidityTest is HyperdriveTest {
hyperdrive.addLiquidity(
contribution,
0,
0,
0.04e18,
IHyperdrive.Options({
destination: bob,
Expand Down Expand Up @@ -217,6 +244,7 @@ contract AddLiquidityTest is HyperdriveTest {
hyperdrive.addLiquidity(
contribution,
0,
0,
0.04e18,
IHyperdrive.Options({
destination: bob,
Expand Down
8 changes: 4 additions & 4 deletions test/units/hyperdrive/CloseShortTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ contract CloseShortTest is HyperdriveTest {
asBase: false,
// NOTE: Roughly double deposit amount needed to cover 100% flat fee
depositAmount: 10e18 * 2,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint128).max,
extraData: new bytes(0)
Expand Down Expand Up @@ -613,7 +613,7 @@ contract CloseShortTest is HyperdriveTest {
asBase: false,
// NOTE: Roughly double deposit amount needed to cover 100% flat fee
depositAmount: 10e18 * 2,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint128).max,
extraData: new bytes(0)
Expand Down Expand Up @@ -661,7 +661,7 @@ contract CloseShortTest is HyperdriveTest {
asBase: false,
// NOTE: Roughly double deposit amount needed to cover 100% flat fee
depositAmount: 10e18 * 2,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint128).max,
extraData: new bytes(0)
Expand Down Expand Up @@ -696,7 +696,7 @@ contract CloseShortTest is HyperdriveTest {
asBase: false,
// NOTE: Roughly double deposit amount needed to cover 100% flat fee
depositAmount: 10e18 * 2,
minVaultSharePrice: 0,
minSharePrice: 0,
minSlippage: 0,
maxSlippage: type(uint128).max,
extraData: new bytes(0)
Expand Down
Loading

0 comments on commit 6b3cb74

Please sign in to comment.