Skip to content
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: v0.3.0
cache: false

- name: forge version
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: v0.3.0

- name: Run Forge build
run: |
Expand Down
53 changes: 35 additions & 18 deletions contracts/EverlongStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -502,27 +502,46 @@ contract EverlongStrategy is BaseStrategy {
""
);

// No conversions are needed for hyperdrive's minimum transaction
// amount when closing longs since it's denominated in bonds.
uint256 minimumBonds = _poolConfig.minimumTransactionAmount;

// Calculate how many bonds out of the position need to be closed.
uint256 bondsNeeded;
if (totalPositionValue <= _targetOutput - output) {
// We need the entire position's bonds.
// We can always assume it's at least the minimum transaction
// amount.
bondsNeeded = uint256(position.bondAmount);
} else {
// `bondsNeeded <= position.bondAmount` since
// `_targetOutput - output < totalPositionValue`.
bondsNeeded = uint256(position.bondAmount).mulDivUp(
(_targetOutput - output),
totalPositionValue
);

// We need to close at least the `minimumTransactionAmount`
// value of bonds. It's safe to assume that the position
// has at least that much.
bondsNeeded = bondsNeeded.max(minimumBonds);

// If closing bondsNeeded would leave a position with less than
// `minimumTransactionAmount`, close the entire position
// instead.
if (position.bondAmount - bondsNeeded < minimumBonds) {
bondsNeeded = position.bondAmount;
}
}

// Close only part of the position if there are sufficient bonds
// to reach the target output without leaving a small amount left.
// For this case, the remaining bonds must be worth at least
// Hyperdrive's minimum transaction amount.
if (
totalPositionValue >
(_targetOutput - output + minimumTransactionAmount()).mulUp(
ONE + partialPositionClosureBuffer
)
) {
// Calculate the amount of bonds to close from the position.
uint256 bondsNeeded = uint256(position.bondAmount).mulDivUp(
(_targetOutput - output).mulUp(
ONE + partialPositionClosureBuffer
),
totalPositionValue
);

if (bondsNeeded < position.bondAmount) {
// Close part of the position.
//
// Since this functino would never be called as part of a
// Since this function would never be called as part of a
// `tend()`, there's no need to retrieve the `TendConfig` and
// set the slippage guard.
//
Expand All @@ -541,9 +560,7 @@ contract EverlongStrategy is BaseStrategy {

// No more closures are needed.
return output;
}
// Close the entire position.
else {
} else {
// Close the entire position.
//
// Since this function would never be called as part of a
Expand Down
36 changes: 36 additions & 0 deletions test/everlong/EverlongForkSDAITest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,29 @@ contract EverlongForkSDAITest is EverlongTest {
vm.stopPrank();
}

/// @dev Deposit into the SDAI everlong strategy.
/// @param _assets Amount of assets to deposit.
/// @param _from Source of the tokens.
/// @return shares Amount of shares received from the deposit.
function depositSDAIStrategy(
uint256 _assets,
address _from
) internal returns (uint256 shares) {
// Mint _from some SDAI.
mintSDAI(_assets, _from);

// Enable deposits from _from.
vm.startPrank(management);
strategy.setDepositor(_from, true);
vm.stopPrank();

// Make the approval and deposit.
vm.startPrank(_from);
asset.approve(address(strategy), _assets);
shares = strategy.deposit(_assets, _from);
vm.stopPrank();
}

/// @dev Redeem shares from the SDAI everlong vault.
/// @param _shares Amount of shares to redeem.
/// @param _from Source of the shares.
Expand All @@ -69,4 +92,17 @@ contract EverlongForkSDAITest is EverlongTest {
assets = vault.redeem(_shares, _from, _from);
vm.stopPrank();
}

/// @dev Redeem shares from the SDAI everlong strategy.
/// @param _shares Amount of shares to redeem.
/// @param _from Source of the shares.
/// @return assets Amount of assets received from the redemption.
function redeemSDAIStrategy(
uint256 _shares,
address _from
) internal returns (uint256 assets) {
vm.startPrank(_from);
assets = strategy.redeem(_shares, _from, _from);
vm.stopPrank();
}
}
29 changes: 28 additions & 1 deletion test/everlong/integration/PartialClosures.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ contract TestPartialClosures is EverlongTest {
// Alice deposits into Everlong.
uint256 aliceDepositAmount = bound(
_deposit,
MINIMUM_TRANSACTION_AMOUNT * 100, // Increase minimum bound otherwise partial redemption won't occur
IEverlongStrategy(address(strategy)).minimumTransactionAmount() *
100, // Increase minimum bound otherwise partial redemption won't occur
hyperdrive.calculateMaxLong(AS_BASE)
);
uint256 aliceShares = depositStrategy(aliceDepositAmount, alice, true);
Expand Down Expand Up @@ -114,4 +115,30 @@ contract TestPartialClosures is EverlongTest {
// Ensure Everlong has one position left.
assertEq(IEverlongStrategy(address(strategy)).positionCount(), 1);
}

/// @dev Tests that when a partial closure would result in a remaining
/// position value less than the minimum transaction amount, the entire
/// position is closed.
function test_partial_closures_position_min_transaction_amount() external {
// Alice deposits into Everlong.
uint256 aliceDepositAmount = 1000e18;
uint256 aliceShares = depositStrategy(aliceDepositAmount, alice, true);

// Ensure there is now one position.
assertEq(IEverlongStrategy(address(strategy)).positionCount(), 1);

// Calculate how many shares are neeed to reach the minimum transaction
// amount.
uint256 minTxShareAmount = (aliceShares).mulDivDown(
hyperdrive.getPoolConfig().minimumTransactionAmount,
IEverlongStrategy(address(strategy)).positionAt(0).bondAmount
);

// Redeem shares such that the remaining share value should be less
// than the minimum transaction amount.
redeemStrategy(aliceShares - minTxShareAmount, alice, true);

// There should be no positions left.
assertEq(IEverlongStrategy(address(strategy)).positionCount(), 0);
}
}
31 changes: 31 additions & 0 deletions test/everlong/units/EverlongForkSDAITest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ pragma solidity ^0.8.20;
import { console2 as console } from "forge-std/console2.sol";
import { IERC20, IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol";
import { ILido } from "hyperdrive/contracts/src/interfaces/ILido.sol";
import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol";
import { IEverlongStrategy } from "../../../contracts/interfaces/IEverlongStrategy.sol";
import { EVERLONG_STRATEGY_KIND, EVERLONG_VERSION } from "../../../contracts/libraries/Constants.sol";
import { EverlongForkSDAITest } from "../EverlongForkSDAITest.sol";

/// @dev Tests Everlong functionality when using the existing SDAIHyperdrive
/// instance on a fork.
contract TestEverlongForkSDAI is EverlongForkSDAITest {
using FixedPointMath for uint256;

/// @dev Ensure the deposit functions work as expected.
function test_deposit() external {
// Alice and Bob deposit into the vault.
Expand Down Expand Up @@ -72,4 +75,32 @@ contract TestEverlongForkSDAI is EverlongForkSDAITest {
assertGt(maturityTime, 0);
assertGt(bondAmount, 0);
}

/// @dev Tests that when a partial closure would result in a remaining
/// position value less than the minimum transaction amount, the entire
/// position is closed.
function test_partial_closures_min_transaction_amount() external {
// Alice deposits into Everlong.
uint256 aliceDepositAmount = 10e18;
uint256 aliceShares = depositSDAIStrategy(aliceDepositAmount, alice);
rebalance();

// Ensure there is now one position.
assertEq(IEverlongStrategy(address(strategy)).positionCount(), 1);

// Calculate how many shares are neeed to reach the minimum transaction
// amount.
uint256 minTxShareAmount = (aliceShares).mulDivDown(
hyperdrive.getPoolConfig().minimumTransactionAmount,
IEverlongStrategy(address(strategy)).positionAt(0).bondAmount
);

// Redeem shares such that the remaining share value should be less
// than the minimum transaction amount.
redeemSDAIStrategy(aliceShares - minTxShareAmount, alice);
rebalance();

// There should be no positions left.
assertEq(IEverlongStrategy(address(strategy)).positionCount(), 0);
}
}
Loading