beautified

This commit is contained in:
giteadmin 2025-07-08 10:33:10 +02:00
parent 8de3865c6f
commit 77af20dcee
14 changed files with 496 additions and 335 deletions

View file

@ -3,7 +3,6 @@ pragma solidity ^0.8.19;
import {DeployScript} from "./DeployScript.sol";
contract BaseDeploy is DeployScript {
function setUp() public {
// Base data
feeDest = 0x31ea4993dd336158E1536a1851b76B738BDd24c8;

View file

@ -3,7 +3,6 @@ pragma solidity ^0.8.19;
import {DeployScript} from "./DeployScript.sol";
contract BaseSepoliaDeploy is DeployScript {
function setUp() public {
// Base Sepolia data
feeDest = 0xf6a3eef9088A255c32b6aD2025f83E57291D9011;

View file

@ -34,14 +34,14 @@ contract DeployScript is Script {
IUniswapV3Factory factory = IUniswapV3Factory(v3Factory);
address liquidityPool = factory.createPool(weth, address(harb), FEE);
IUniswapV3Pool(liquidityPool).initializePoolFor1Cent(token0isWeth);
Optimizer optimizer = new Optimizer();
bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(harb),address(stake));
ERC1967Proxy proxy = new ERC1967Proxy(address(optimizer), params);
Optimizer optimizer = new Optimizer();
bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(harb), address(stake));
ERC1967Proxy proxy = new ERC1967Proxy(address(optimizer), params);
LiquidityManager liquidityManager = new LiquidityManager(v3Factory, weth, address(harb), address(proxy));
liquidityManager.setFeeDestination(feeDest);
// note: this delayed initialization is not a security issue.
harb.setLiquidityManager(address(liquidityManager));
(bool sent, ) = address(liquidityManager).call{value: 0.01 ether}("");
(bool sent,) = address(liquidityManager).call{value: 0.01 ether}("");
require(sent, "Failed to send Ether");
//TODO: wait few minutes and call recenter
vm.stopBroadcast();

View file

@ -12,6 +12,7 @@ import {Math} from "@openzeppelin/utils/math/Math.sol";
contract Harberg is ERC20, ERC20Permit {
using Math for uint256;
// Minimum fraction of the total supply required for staking to prevent fragmentation of staking positions
uint256 private constant MIN_STAKE_FRACTION = 3000;
// Address of the liquidity manager
address private liquidityManager;
@ -36,9 +37,7 @@ contract Harberg is ERC20, ERC20Permit {
* @param name_ The name of the token
* @param symbol_ The symbol of the token
*/
constructor(string memory name_, string memory symbol_)
ERC20(name_, symbol_)
ERC20Permit(name_){}
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) ERC20Permit(name_) {}
/**
* @notice Sets the address for the liquidityManager. Used once post-deployment to initialize the contract.
@ -91,7 +90,7 @@ contract Harberg is ERC20, ERC20Permit {
// make sure staking pool grows proportional to economy
uint256 stakingPoolBalance = balanceOf(stakingPool);
if (stakingPoolBalance > 0) {
uint256 newStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance);
uint256 newStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance);
_mint(stakingPool, newStake);
}
_mint(address(liquidityManager), _amount);
@ -134,5 +133,4 @@ contract Harberg is ERC20, ERC20Permit {
function outstandingSupply() public view returns (uint256) {
return totalSupply() - balanceOf(liquidityManager);
}
}

View file

@ -24,7 +24,7 @@ contract Optimizer is Initializable, UUPSUpgradeable {
* @param _harberg The address of the Harberg token.
* @param _stake The address of the Stake contract.
*/
function initialize(address _harberg, address _stake) initializer public {
function initialize(address _harberg, address _stake) public initializer {
// Set the admin for upgradeability (using ERC1967Upgrade _changeAdmin)
_changeAdmin(msg.sender);
harberg = Harberg(_harberg);
@ -50,10 +50,11 @@ contract Optimizer is Initializable, UUPSUpgradeable {
* @param percentageStaked The percentage (in 1e18 precision) of the authorized stake that is currently staked.
* @return sentimentValue A value in the range 0 to 1e18 where 1e18 represents the worst sentiment.
*/
function calculateSentiment(
uint256 averageTaxRate,
uint256 percentageStaked
) public pure returns (uint256 sentimentValue) {
function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked)
public
pure
returns (uint256 sentimentValue)
{
// deltaS is the slack available below full staking
uint256 deltaS = 1e18 - percentageStaked;
@ -99,12 +100,7 @@ contract Optimizer is Initializable, UUPSUpgradeable {
function getLiquidityParams()
external
view
returns (
uint256 capitalInefficiency,
uint256 anchorShare,
uint24 anchorWidth,
uint256 discoveryDepth
)
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
uint256 percentageStaked = stake.getPercentageStaked();
uint256 averageTaxRate = stake.getAverageTaxRate();
@ -116,4 +112,3 @@ contract Optimizer is Initializable, UUPSUpgradeable {
discoveryDepth = sentiment;
}
}

View file

@ -3,7 +3,7 @@ pragma solidity ^0.8.19;
import {IERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
import {IERC20Metadata} from "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
import {ERC20Permit} from"@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
import {Math} from "@openzeppelin/utils/math/Math.sol";
import {Harberg} from "./Harberg.sol";
@ -16,14 +16,14 @@ error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStak
* @notice This contract manages the staking positions for the Harberg token, allowing users to stake tokens
* in exchange for a share of the total supply. Stakers can set and adjust tax rates on their stakes,
* which affect the Universal Basic Income (UBI) paid from the tax pool.
*
*
* The contract handles:
* - Creation of staking positions with specific tax rates.
* - Snatching of existing positions under certain conditions to consolidate stakes.
* - Calculation and payment of taxes based on stake duration and tax rate.
* - Adjustment of tax rates with protections against griefing through rapid changes.
* - Exiting of positions, either partially or fully, returning the staked assets to the owner.
*
*
* Tax rates and staking positions are adjustable, with a mechanism to prevent snatch-grieving by
* enforcing a minimum tax payment duration.
*/
@ -37,7 +37,38 @@ contract Stake {
uint256 internal constant MAX_STAKE = 20; // 20% of HARB supply
uint256 internal constant TAX_FLOOR_DURATION = 60 * 60 * 24 * 3; //this duration is the minimum basis for fee calculation, regardless of actual holding time.
// the tax rates are discrete to prevent users from snatching by micro incroments of tax
uint256[] public TAX_RATES = [1, 3, 5, 8, 12, 18, 24, 30, 40, 50, 60, 80, 100, 130, 180, 250, 320, 420, 540, 700, 920, 1200, 1600, 2000, 2600, 3400, 4400, 5700, 7500, 9700];
uint256[] public TAX_RATES = [
1,
3,
5,
8,
12,
18,
24,
30,
40,
50,
60,
80,
100,
130,
180,
250,
320,
420,
540,
700,
920,
1200,
1600,
2000,
2600,
3400,
4400,
5700,
7500,
9700
];
// this is the base for the values in the array above: e.g. 1/100 = 1%
uint256 internal constant TAX_RATE_BASE = 100;
/**
@ -49,8 +80,12 @@ contract Stake {
error NoPermission(address requester, address owner);
error PositionNotFound(uint256 positionId, address requester);
event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 harbergDeposit, uint256 share, uint32 taxRate);
event PositionTaxPaid(uint256 indexed positionId, address indexed owner, uint256 taxPaid, uint256 newShares, uint256 taxRate);
event PositionCreated(
uint256 indexed positionId, address indexed owner, uint256 harbergDeposit, uint256 share, uint32 taxRate
);
event PositionTaxPaid(
uint256 indexed positionId, address indexed owner, uint256 taxPaid, uint256 newShares, uint256 taxRate
);
event PositionRateHiked(uint256 indexed positionId, address indexed owner, uint256 newTaxRate);
event PositionShrunk(uint256 indexed positionId, address indexed owner, uint256 newShares, uint256 harbergPayout);
event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 harbergPayout);
@ -101,7 +136,8 @@ contract Stake {
: block.timestamp;
uint256 elapsedTime = ihet - pos.lastTaxTime;
uint256 assetsBefore = sharesToAssets(pos.share);
uint256 taxAmountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
uint256 taxAmountDue =
assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
if (taxAmountDue >= assetsBefore) {
// can not pay more tax than value of position
taxAmountDue = assetsBefore;
@ -143,7 +179,7 @@ contract Stake {
/// @dev Internal function to reduce the size of a staking position by a specified number of shares, transferring the corresponding Harberg tokens to the owner.
function _shrinkPosition(uint256 positionId, StakingPosition storage pos, uint256 sharesToTake) private {
require (sharesToTake < pos.share, "position too small");
require(sharesToTake < pos.share, "position too small");
uint256 assets = sharesToAssets(sharesToTake);
pos.share -= sharesToTake;
totalSharesAtTaxRate[pos.taxRate] -= sharesToTake;
@ -287,9 +323,7 @@ contract Stake {
uint8 v,
bytes32 r,
bytes32 s
) external
returns (uint256 positionId)
{
) external returns (uint256 positionId) {
ERC20Permit(address(harberg)).permit(receiver, address(this), assets, deadline, v, r, s);
return snatch(assets, receiver, taxRate, positionsToSnatch);
}
@ -360,17 +394,16 @@ contract Stake {
/// @return averageTaxRate A number between 0 and 1e18 indicating the average tax rate.
function getAverageTaxRate() external view returns (uint256 averageTaxRate) {
// Compute average tax rate weighted by shares
averageTaxRate = 0;
if (outstandingStake > 0) {
for (uint256 i = 0; i < TAX_RATES.length; i++) {
averageTaxRate += TAX_RATES[i] * totalSharesAtTaxRate[i];
}
averageTaxRate = averageTaxRate / outstandingStake;
// normalize tax rate
averageTaxRate = averageTaxRate * 1e18 / TAX_RATES[TAX_RATES.length - 1];
}
if (outstandingStake > 0) {
for (uint256 i = 0; i < TAX_RATES.length; i++) {
averageTaxRate += TAX_RATES[i] * totalSharesAtTaxRate[i];
}
averageTaxRate = averageTaxRate / outstandingStake;
// normalize tax rate
averageTaxRate = averageTaxRate * 1e18 / TAX_RATES[TAX_RATES.length - 1];
}
}
/// @notice Computes the percentage of Harberg staked from outstanding Stake and authorized Stake.
@ -378,5 +411,4 @@ contract Stake {
function getPercentageStaked() external view returns (uint256 percentageStaked) {
percentageStaked = (outstandingStake * 1e18) / authorizedStake();
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;
import '@openzeppelin/token/ERC20/IERC20.sol';
import "@openzeppelin/token/ERC20/IERC20.sol";
/// @title Interface for WETH9
interface IWETH9 is IERC20 {

View file

@ -30,7 +30,7 @@ contract HarbergTest is Test {
// Simulates unstaking by transferring tokens from the stakingPool back to a given address.
function simulateUnstake(uint256 amount) internal {
// Direct transfer from the stakingPool to 'to' address to simulate unstaking
vm.prank(stakingPool); // Assuming 'stake' contract would allow this in an actual scenario
vm.prank(stakingPool); // Assuming 'stake' contract would allow this in an actual scenario
harberg.transfer(address(this), amount);
}
@ -127,7 +127,8 @@ contract HarbergTest is Test {
uint256 initialStakingPoolBalance = harberg.balanceOf(stakingPool);
mintAmount = bound(mintAmount, 1, 500 * 1e18);
uint256 expectedNewStake = initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance);
uint256 expectedNewStake =
initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance);
// Expect Transfer events
vm.expectEmit(true, true, true, true, address(harberg));
@ -138,7 +139,11 @@ contract HarbergTest is Test {
uint256 expectedStakingPoolBalance = initialStakingPoolBalance + expectedNewStake;
uint256 expectedTotalSupply = initialTotalSupply + mintAmount + expectedNewStake;
assertEq(harberg.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after mint.");
assertEq(
harberg.balanceOf(stakingPool),
expectedStakingPoolBalance,
"Staking pool balance did not adjust correctly after mint."
);
assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply did not match expected after mint.");
}
@ -159,7 +164,8 @@ contract HarbergTest is Test {
burnAmount = bound(burnAmount, 0, 200 * 1e18);
uint256 initialTotalSupply = harberg.totalSupply();
uint256 initialStakingPoolBalance = harberg.balanceOf(stakingPool);
uint256 expectedExcessStake = initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance);
uint256 expectedExcessStake =
initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance);
vm.prank(address(liquidityManager));
harberg.burn(burnAmount);
@ -167,7 +173,11 @@ contract HarbergTest is Test {
uint256 expectedStakingPoolBalance = initialStakingPoolBalance - expectedExcessStake;
uint256 expectedTotalSupply = initialTotalSupply - burnAmount - expectedExcessStake;
assertEq(harberg.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after burn.");
assertEq(
harberg.balanceOf(stakingPool),
expectedStakingPoolBalance,
"Staking pool balance did not adjust correctly after burn."
);
assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply did not match expected after burn.");
}
}

View file

@ -10,7 +10,6 @@ pragma solidity ^0.8.19;
* - Edge case classification and recovery
* @dev Uses setUp() pattern for consistent test initialization
*/
import "forge-std/Test.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
@ -45,43 +44,45 @@ uint256 constant MIN_TRADE_AMOUNT = 1 ether;
// Error handling constants
bytes32 constant AMPLITUDE_ERROR = keccak256("amplitude not reached.");
bytes32 constant EXPENSIVE_HARB_ERROR = keccak256("HARB extremely expensive: perform swap to normalize price before recenter");
bytes32 constant PROTOCOL_DEATH_ERROR = keccak256("Protocol death: Insufficient ETH reserves to support HARB at extremely low prices");
bytes32 constant EXPENSIVE_HARB_ERROR =
keccak256("HARB extremely expensive: perform swap to normalize price before recenter");
bytes32 constant PROTOCOL_DEATH_ERROR =
keccak256("Protocol death: Insufficient ETH reserves to support HARB at extremely low prices");
// Dummy.sol
contract Dummy {
// This contract can be empty as it is only used to affect the nonce
// This contract can be empty as it is only used to affect the nonce
}
contract LiquidityManagerTest is UniswapTestBase {
// Setup configuration
bool constant DEFAULT_TOKEN0_IS_WETH = false;
uint256 constant DEFAULT_ACCOUNT_BALANCE = 300 ether;
// Flag to skip automatic setUp for tests that need custom setup
bool private _skipAutoSetup;
using UniswapHelpers for IUniswapV3Pool;
IUniswapV3Factory factory;
Stake stake;
LiquidityManager lm;
address feeDestination = makeAddr("fees");
struct Response {
uint256 ethFloor;
uint256 ethAnchor;
uint256 ethDiscovery;
uint256 harbergFloor;
uint256 harbergAnchor;
uint256 harbergDiscovery;
uint256 ethFloor;
uint256 ethAnchor;
uint256 ethDiscovery;
uint256 harbergFloor;
uint256 harbergAnchor;
uint256 harbergDiscovery;
}
/// @notice Utility to deploy dummy contracts for address manipulation
/// @param count Number of dummy contracts to deploy
/// @dev Used to manipulate contract deployment addresses for token ordering
function deployDummies(uint count) internal {
for (uint i = 0; i < count; i++) {
function deployDummies(uint256 count) internal {
for (uint256 i = 0; i < count; i++) {
new Dummy(); // Just increment the nonce
}
}
@ -95,18 +96,18 @@ contract LiquidityManagerTest is UniswapTestBase {
_deployProtocolContracts();
_configurePermissions();
}
/// @notice Deploys the Uniswap factory
function _deployFactory() internal {
factory = UniswapHelpers.deployUniswapFactory();
}
/// @notice Deploys tokens in the specified order
/// @param token0shouldBeWeth Whether token0 should be WETH
function _deployTokensWithOrder(bool token0shouldBeWeth) internal {
bool setupComplete = false;
uint retryCount = 0;
uint256 retryCount = 0;
while (!setupComplete && retryCount < 5) {
// Clean slate if retrying
if (retryCount > 0) {
@ -128,14 +129,14 @@ contract LiquidityManagerTest is UniswapTestBase {
}
require(setupComplete, "Setup failed to meet the condition after several retries");
}
/// @notice Creates and initializes the Uniswap pool
function _createAndInitializePool() internal {
pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE));
token0isWeth = address(weth) < address(harberg);
pool.initializePoolFor1Cent(token0isWeth);
}
/// @notice Deploys protocol contracts (Stake, Optimizer, LiquidityManager)
function _deployProtocolContracts() internal {
stake = new Stake(address(harberg), feeDestination);
@ -144,7 +145,7 @@ contract LiquidityManagerTest is UniswapTestBase {
lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(optimizer));
lm.setFeeDestination(feeDestination);
}
/// @notice Configures permissions and initial funding
function _configurePermissions() internal {
harberg.setStakingPool(address(stake));
@ -160,17 +161,17 @@ contract LiquidityManagerTest is UniswapTestBase {
_handleExtremePrice();
_attemptRecenter(last);
}
/// @notice Updates oracle time to ensure accurate price data
function _updateOracleTime() internal {
uint256 timeBefore = block.timestamp;
vm.warp(timeBefore + ORACLE_UPDATE_INTERVAL);
}
/// @notice Handles extreme price conditions with normalizing swaps
function _handleExtremePrice() internal {
(, int24 currentTick, , , , , ) = pool.slot0();
(, int24 currentTick,,,,,) = pool.slot0();
if (_isExtremelyExpensive(currentTick)) {
console.log("Detected extremely expensive HARB, performing normalizing swap...");
_performNormalizingSwap(currentTick, true);
@ -179,7 +180,7 @@ contract LiquidityManagerTest is UniswapTestBase {
_performNormalizingSwap(currentTick, false);
}
}
/// @notice Attempts the recenter operation with proper error handling
/// @param last Whether this is the last attempt (affects error handling)
function _attemptRecenter(bool last) internal {
@ -189,37 +190,43 @@ contract LiquidityManagerTest is UniswapTestBase {
_handleRecenterError(reason, last);
}
}
/// @notice Checks if HARB price is extremely expensive
/// @param currentTick The current price tick
/// @return True if HARB is extremely expensive
function _isExtremelyExpensive(int24 currentTick) internal pure returns (bool) {
return currentTick >= TickMath.MAX_TICK - EXTREME_PRICE_MARGIN;
}
/// @notice Checks if HARB price is extremely cheap
/// @param currentTick The current price tick
/// @return True if HARB is extremely cheap
function _isExtremelyCheap(int24 currentTick) internal pure returns (bool) {
return currentTick <= TickMath.MIN_TICK + EXTREME_PRICE_MARGIN;
}
/// @notice Validates recenter operation results
/// @param isUp Whether the recenter moved positions up or down
function _validateRecenterResult(bool isUp) internal view {
Response memory liquidityResponse = checkLiquidity(isUp ? "shift" : "slide");
assertGt(liquidityResponse.ethFloor, liquidityResponse.ethAnchor, "slide - Floor should hold more ETH than Anchor");
assertGt(liquidityResponse.harbergDiscovery, liquidityResponse.harbergAnchor * 5, "slide - Discovery should hold more HARB than Anchor");
assertGt(
liquidityResponse.ethFloor, liquidityResponse.ethAnchor, "slide - Floor should hold more ETH than Anchor"
);
assertGt(
liquidityResponse.harbergDiscovery,
liquidityResponse.harbergAnchor * 5,
"slide - Discovery should hold more HARB than Anchor"
);
assertEq(liquidityResponse.harbergFloor, 0, "slide - Floor should have no HARB");
assertEq(liquidityResponse.ethDiscovery, 0, "slide - Discovery should have no ETH");
}
/// @notice Handles recenter operation errors
/// @param reason The error reason string
/// @param last Whether this is the last attempt
function _handleRecenterError(string memory reason, bool last) internal view {
bytes32 errorHash = keccak256(abi.encodePacked(reason));
if (errorHash == AMPLITUDE_ERROR) {
console.log("slide failed on amplitude");
} else if (errorHash == EXPENSIVE_HARB_ERROR) {
@ -237,54 +244,53 @@ contract LiquidityManagerTest is UniswapTestBase {
}
}
}
/// @notice Performs a normalizing swap to bring extreme prices back to manageable levels
/// @param currentTick The current tick position
/// @param isExpensive True if HARB is extremely expensive, false if extremely cheap
function _performNormalizingSwap(int24 currentTick, bool isExpensive) internal {
console.log("Current tick before normalization:", vm.toString(currentTick));
if (isExpensive) {
// HARB is extremely expensive - we need to bring the price DOWN
// This means we need to SELL HARB for ETH (not buy HARB with ETH)
// Get HARB balance from account (who has been buying) to use for normalization
uint256 accountHarbBalance = harberg.balanceOf(account);
if (accountHarbBalance > 0) {
uint256 harbToSell = accountHarbBalance / NORMALIZATION_SELL_PERCENTAGE; // Sell 1% of account's HARB balance
if (harbToSell == 0) harbToSell = 1; // Minimum 1 wei
vm.prank(account);
harberg.transfer(address(this), harbToSell);
console.log("Performing normalizing swap: selling", vm.toString(harbToSell), "HARB to bring price down");
// Approve for swap
harberg.approve(address(pool), harbToSell);
// Swap should work - if it doesn't, there's a fundamental problem
performSwap(harbToSell, false); // false = selling HARB for ETH
} else {
console.log("No HARB balance available for normalization");
}
} else {
// HARB is extremely cheap - we need to bring the price UP
// This means we need to BUY HARB with ETH (not sell HARB)
uint256 ethToBuy = NORMALIZATION_BUY_AMOUNT; // Small amount for price normalization
// Ensure we have enough ETH
if (weth.balanceOf(address(this)) < ethToBuy) {
vm.deal(address(this), ethToBuy);
weth.deposit{value: ethToBuy}();
}
console.log("Performing normalizing swap: buying HARB with", vm.toString(ethToBuy), "ETH to bring price up");
performSwap(ethToBuy, true); // true = buying HARB with ETH
}
// Check the new price
(, int24 newTick, , , , , ) = pool.slot0();
(, int24 newTick,,,,,) = pool.slot0();
console.log("New tick after normalization:", vm.toString(newTick));
console.log("Price change:", vm.toString(newTick - currentTick), "ticks");
}
@ -293,82 +299,86 @@ contract LiquidityManagerTest is UniswapTestBase {
/// @param s The liquidity stage (FLOOR, ANCHOR, DISCOVERY)
/// @return currentTick Current price tick of the pool
/// @return tickLower Lower bound of the position's price range
/// @return tickUpper Upper bound of the position's price range
/// @return tickUpper Upper bound of the position's price range
/// @return ethAmount Amount of ETH in the position
/// @return harbergAmount Amount of HARB in the position
/// @dev Calculates actual token amounts based on current pool price and position liquidity
function getBalancesPool(LiquidityManager.Stage s) internal view returns (int24 currentTick, int24 tickLower, int24 tickUpper, uint256 ethAmount, uint256 harbergAmount) {
(,tickLower, tickUpper) = lm.positions(s);
(uint128 liquidity, , , ,) = pool.positions(keccak256(abi.encodePacked(address(lm), tickLower, tickUpper)));
function getBalancesPool(LiquidityManager.Stage s)
internal
view
returns (int24 currentTick, int24 tickLower, int24 tickUpper, uint256 ethAmount, uint256 harbergAmount)
{
(, tickLower, tickUpper) = lm.positions(s);
(uint128 liquidity,,,,) = pool.positions(keccak256(abi.encodePacked(address(lm), tickLower, tickUpper)));
// Fetch the current price from the pool
uint160 sqrtPriceX96;
(sqrtPriceX96, currentTick, , , , , ) = pool.slot0();
uint160 sqrtPriceAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtPriceBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
// Fetch the current price from the pool
uint160 sqrtPriceX96;
(sqrtPriceX96, currentTick,,,,,) = pool.slot0();
uint160 sqrtPriceAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtPriceBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
// Calculate amounts based on the current tick position relative to provided ticks
if (token0isWeth) {
if (currentTick < tickLower) {
// Current price is below the lower bound of the liquidity position
ethAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
harbergAmount = 0; // All liquidity is in token0 (ETH)
} else if (currentTick > tickUpper) {
// Current price is above the upper bound of the liquidity position
ethAmount = 0; // All liquidity is in token1 (HARB)
harbergAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
} else {
// Current price is within the bounds of the liquidity position
ethAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
harbergAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
}
} else {
if (currentTick < tickLower) {
// Current price is below the lower bound of the liquidity position
harbergAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
ethAmount = 0; // All liquidity is in token1 (ETH)
} else if (currentTick > tickUpper) {
// Current price is above the upper bound of the liquidity position
harbergAmount = 0; // All liquidity is in token0 (HARB)
ethAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
} else {
// Current price is within the bounds of the liquidity position
harbergAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
ethAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
}
}
// Calculate amounts based on the current tick position relative to provided ticks
if (token0isWeth) {
if (currentTick < tickLower) {
// Current price is below the lower bound of the liquidity position
ethAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
harbergAmount = 0; // All liquidity is in token0 (ETH)
} else if (currentTick > tickUpper) {
// Current price is above the upper bound of the liquidity position
ethAmount = 0; // All liquidity is in token1 (HARB)
harbergAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
} else {
// Current price is within the bounds of the liquidity position
ethAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
harbergAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
}
} else {
if (currentTick < tickLower) {
// Current price is below the lower bound of the liquidity position
harbergAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
ethAmount = 0; // All liquidity is in token1 (ETH)
} else if (currentTick > tickUpper) {
// Current price is above the upper bound of the liquidity position
harbergAmount = 0; // All liquidity is in token0 (HARB)
ethAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
} else {
// Current price is within the bounds of the liquidity position
harbergAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
ethAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
}
}
}
/// @notice Checks and validates current liquidity positions across all stages
/// @return liquidityResponse Structure containing ETH and HARB amounts for each position
/// @dev Aggregates position data from FLOOR, ANCHOR, and DISCOVERY stages
function checkLiquidity(string memory /* eventName */) internal view returns (Response memory) {
Response memory liquidityResponse;
int24 currentTick;
{
int24 tickLower;
int24 tickUpper;
uint256 eth;
uint256 harb;
{
(currentTick, tickLower, tickUpper, eth, harb) = getBalancesPool(LiquidityManager.Stage.FLOOR);
liquidityResponse.ethFloor = eth;
liquidityResponse.harbergFloor = harb;
}
{
(,tickLower, tickUpper, eth, harb) = getBalancesPool(LiquidityManager.Stage.ANCHOR);
liquidityResponse.ethAnchor = eth;
liquidityResponse.harbergAnchor = harb;
}
{
(,tickLower, tickUpper, eth, harb) = getBalancesPool(LiquidityManager.Stage.DISCOVERY);
liquidityResponse.ethDiscovery = eth;
liquidityResponse.harbergDiscovery = harb;
}
}
function checkLiquidity(string memory /* eventName */ ) internal view returns (Response memory) {
Response memory liquidityResponse;
int24 currentTick;
return liquidityResponse;
{
int24 tickLower;
int24 tickUpper;
uint256 eth;
uint256 harb;
{
(currentTick, tickLower, tickUpper, eth, harb) = getBalancesPool(LiquidityManager.Stage.FLOOR);
liquidityResponse.ethFloor = eth;
liquidityResponse.harbergFloor = harb;
}
{
(, tickLower, tickUpper, eth, harb) = getBalancesPool(LiquidityManager.Stage.ANCHOR);
liquidityResponse.ethAnchor = eth;
liquidityResponse.harbergAnchor = harb;
}
{
(, tickLower, tickUpper, eth, harb) = getBalancesPool(LiquidityManager.Stage.DISCOVERY);
liquidityResponse.ethDiscovery = eth;
liquidityResponse.harbergDiscovery = harb;
}
}
return liquidityResponse;
}
/// @notice Executes a buy operation (ETH -> HARB)
@ -391,34 +401,27 @@ contract LiquidityManagerTest is UniswapTestBase {
/// @dev Required for WETH unwrapping operations during testing
receive() external payable {}
// ========================================
// OVERFLOW AND ARITHMETIC TESTS
// ========================================
/// @notice Tests overflow handling in cumulative calculations
/// @dev Simulates extreme values that could cause arithmetic overflow
function testHandleCumulativeOverflow() public {
_setupCustom(false, 201 ether);
vm.store(
address(lm),
bytes32(uint256(0)),
bytes32(uint256(type(uint256).max - 10))
);
vm.store(
address(lm),
bytes32(uint256(1)),
bytes32(uint256((type(uint256).max - 10) / (3000 * 10**20)))
);
vm.store(address(lm), bytes32(uint256(0)), bytes32(uint256(type(uint256).max - 10)));
vm.store(address(lm), bytes32(uint256(1)), bytes32(uint256((type(uint256).max - 10) / (3000 * 10 ** 20))));
uint256 cumulativeVolumeWeightedPriceX96 = lm.cumulativeVolumeWeightedPriceX96();
uint256 beforeCumulativeVolume = lm.cumulativeVolume();
assertGt(cumulativeVolumeWeightedPriceX96, type(uint256).max / 2, "Initial cumulativeVolumeWeightedPrice is not near max uint256");
assertGt(
cumulativeVolumeWeightedPriceX96,
type(uint256).max / 2,
"Initial cumulativeVolumeWeightedPrice is not near max uint256"
);
buy(25 ether);
@ -432,7 +435,10 @@ contract LiquidityManagerTest is UniswapTestBase {
// Assert that the price is reasonable
uint256 calculatedPrice = cumulativeVolumeWeightedPriceX96 / cumulativeVolume;
assertTrue(calculatedPrice > 0 && calculatedPrice < 10**40, "Calculated price after wrap-around is not within a reasonable range");
assertTrue(
calculatedPrice > 0 && calculatedPrice < 10 ** 40,
"Calculated price after wrap-around is not within a reasonable range"
);
}
function setUp() public {
@ -440,63 +446,62 @@ contract LiquidityManagerTest is UniswapTestBase {
_commonSetup(DEFAULT_TOKEN0_IS_WETH, DEFAULT_ACCOUNT_BALANCE);
}
}
/// @notice Call this in tests that need custom setup to skip automatic setUp
function _skipSetup() internal {
_skipAutoSetup = true;
}
/// @notice Grant recenter access for testing (commonly needed)
function _grantRecenterAccess() internal {
vm.prank(feeDestination);
lm.setRecenterAccess(address(this));
}
/// @notice Setup with custom parameters but standard flow
function _setupCustom(bool token0IsWeth, uint256 accountBalance) internal {
_skipSetup();
_commonSetup(token0IsWeth, accountBalance);
}
/// @notice Common setup for most tests
/// @param token0IsWeth Whether token0 should be WETH
/// @param accountBalance How much ETH to give to account
function _commonSetup(bool token0IsWeth, uint256 accountBalance) internal {
setUpCustomToken0(token0IsWeth);
// Fund account and convert to WETH
vm.deal(account, accountBalance);
vm.prank(account);
weth.deposit{value: accountBalance}();
// Grant recenter access to bypass oracle checks
vm.prank(feeDestination);
lm.setRecenterAccess(address(this));
// Setup initial liquidity
recenter(false);
}
// ========================================
// EXTREME PRICE HANDLING TESTS
// ========================================
/// @notice Tests handling of extremely expensive HARB prices near MAX_TICK
/// @dev Validates client-side price detection and normalization swaps
function testExtremeExpensiveHarbHandling() public {
// Record initial state
(, int24 initialTick, , , , , ) = pool.slot0();
(, int24 initialTick,,,,,) = pool.slot0();
console.log("Initial tick:", vm.toString(initialTick));
// Buy large amount to push price to extreme
console.log("\n=== PHASE 1: Push to extreme expensive HARB ===");
buy(200 ether);
(, int24 postBuyTick, , , , , ) = pool.slot0();
(, int24 postBuyTick,,,,,) = pool.slot0();
console.log("Tick after large buy:", vm.toString(postBuyTick));
console.log("Price moved:", vm.toString(postBuyTick - initialTick), "ticks higher");
// Test client-side detection and normalization
console.log("\n=== PHASE 2: Test client-side normalization ===");
if (postBuyTick >= TickMath.MAX_TICK - EXTREME_PRICE_MARGIN) {
@ -508,32 +513,32 @@ contract LiquidityManagerTest is UniswapTestBase {
uint256 remainingEth = weth.balanceOf(account);
if (remainingEth > MIN_TRADE_AMOUNT) {
buy(remainingEth / BALANCE_DIVISOR);
(, postBuyTick, , , , , ) = pool.slot0();
(, postBuyTick,,,,,) = pool.slot0();
console.log("Tick after additional buy:", vm.toString(postBuyTick));
}
}
// The intelligent recenter should detect extreme price and normalize
console.log("\n=== PHASE 3: Test intelligent recenter ===");
recenter(false);
(, int24 postRecenterTick, , , , , ) = pool.slot0();
(, int24 postRecenterTick,,,,,) = pool.slot0();
console.log("Tick after recenter:", vm.toString(postRecenterTick));
// Test selling back
console.log("\n=== PHASE 4: Test selling back ===");
uint256 harbBalance = harberg.balanceOf(account);
if (harbBalance > 0) {
sell(harbBalance);
(, int24 finalTick, , , , , ) = pool.slot0();
(, int24 finalTick,,,,,) = pool.slot0();
console.log("Final tick after sell:", vm.toString(finalTick));
}
console.log("\n=== RESULTS ===");
console.log("[SUCCESS] Extreme price handling: PASSED");
console.log("[SUCCESS] Client-side normalization: PASSED");
console.log("[SUCCESS] No arithmetic overflow: PASSED");
// Test passes if we reach here without reverting
}
@ -546,21 +551,29 @@ contract LiquidityManagerTest is UniswapTestBase {
OTHER_ERROR
}
function classifyFailure(bytes memory reason) internal view returns (FailureType failureType, string memory details) {
function classifyFailure(bytes memory reason)
internal
view
returns (FailureType failureType, string memory details)
{
if (reason.length >= 4) {
bytes4 selector = bytes4(reason);
// Note: Error selector logged for debugging when needed
if (selector == 0xae47f702) { // FullMulDivFailed()
return (FailureType.ARITHMETIC_OVERFLOW, "FullMulDivFailed - arithmetic overflow in liquidity calculations");
if (selector == 0xae47f702) {
// FullMulDivFailed()
return (
FailureType.ARITHMETIC_OVERFLOW, "FullMulDivFailed - arithmetic overflow in liquidity calculations"
);
}
if (selector == 0x4e487b71) { // Panic(uint256) - Solidity panic errors
if (selector == 0x4e487b71) {
// Panic(uint256) - Solidity panic errors
if (reason.length >= 36) {
// Extract panic code from the error data
bytes memory sliced = new bytes(32);
for (uint i = 0; i < 32; i++) {
for (uint256 i = 0; i < 32; i++) {
sliced[i] = reason[i + 4];
}
uint256 panicCode = abi.decode(sliced, (uint256));
@ -574,17 +587,18 @@ contract LiquidityManagerTest is UniswapTestBase {
}
return (FailureType.OTHER_ERROR, "Panic: Unknown panic");
}
// Add other specific error selectors as needed
if (selector == 0x54c5b31f) { // Example: "T" error selector
if (selector == 0x54c5b31f) {
// Example: "T" error selector
return (FailureType.TICK_BOUNDARY, "Tick boundary error");
}
}
// Try to decode as string error
if (reason.length > 68) {
bytes memory sliced = new bytes(reason.length - 4);
for (uint i = 0; i < reason.length - 4; i++) {
for (uint256 i = 0; i < reason.length - 4; i++) {
sliced[i] = reason[i + 4];
}
try this.decodeStringError(sliced) returns (string memory errorMsg) {
@ -596,7 +610,7 @@ contract LiquidityManagerTest is UniswapTestBase {
return (FailureType.OTHER_ERROR, "Unknown error");
}
}
return (FailureType.OTHER_ERROR, "Unclassified error");
}
@ -608,7 +622,7 @@ contract LiquidityManagerTest is UniswapTestBase {
// ========================================
// EDGE CASE AND FAILURE CLASSIFICATION TESTS
// ========================================
/// @notice Tests systematic classification of different failure modes
/// @dev Performs multiple trading cycles to trigger various edge cases
function testEdgeCaseClassification() public {
@ -620,10 +634,10 @@ contract LiquidityManagerTest is UniswapTestBase {
uint256 otherErrorCount = 0;
// Perform a series of trades that might push to different edge cases
for (uint i = 0; i < 30; i++) {
for (uint256 i = 0; i < 30; i++) {
uint256 amount = (i * MIN_TRADE_AMOUNT / 10) + MIN_TRADE_AMOUNT;
uint256 harbergBal = harberg.balanceOf(account);
// Trading logic
if (harbergBal == 0) {
amount = amount % (weth.balanceOf(account) / BALANCE_DIVISOR);
@ -642,8 +656,8 @@ contract LiquidityManagerTest is UniswapTestBase {
}
// Check current tick and test recentering
(, int24 currentTick, , , , , ) = pool.slot0();
(, int24 currentTick,,,,,) = pool.slot0();
// Try recentering and classify the result
if (i % 3 == 0) {
try lm.recenter() {
@ -651,12 +665,12 @@ contract LiquidityManagerTest is UniswapTestBase {
console.log("Recenter succeeded at tick:", vm.toString(currentTick));
} catch (bytes memory reason) {
(FailureType failureType, string memory details) = classifyFailure(reason);
if (failureType == FailureType.ARITHMETIC_OVERFLOW) {
arithmeticOverflowCount++;
console.log("Arithmetic overflow at tick:", vm.toString(currentTick));
console.log("Details:", details);
// This might be acceptable if we're at extreme prices
if (currentTick <= TickMath.MIN_TICK + 50000 || currentTick >= TickMath.MAX_TICK - 50000) {
console.log("Overflow at extreme tick - this may be acceptable edge case handling");
@ -688,40 +702,39 @@ contract LiquidityManagerTest is UniswapTestBase {
// ========================================
// PROTOCOL DEATH AND SCENARIO ANALYSIS TESTS
// ========================================
/// @notice Tests distinction between protocol death and recoverable edge cases
/// @dev Analyzes ETH reserves vs outstanding HARB to diagnose scenario type
function testProtocolDeathVsEdgeCase() public {
// Record initial state
uint256 initialEthBalance = address(lm).balance + weth.balanceOf(address(lm));
uint256 initialOutstandingHarb = harberg.outstandingSupply();
(, int24 initialTick, , , , , ) = pool.slot0();
(, int24 initialTick,,,,,) = pool.slot0();
console.log("\n=== INITIAL STATE ===");
console.log("LM ETH balance:", vm.toString(initialEthBalance));
console.log("Outstanding HARB:", vm.toString(initialOutstandingHarb));
console.log("Initial tick:", vm.toString(initialTick));
console.log("ETH/HARB ratio:", vm.toString(initialEthBalance * 1e18 / initialOutstandingHarb));
// Buy large amount to create extreme scenario
console.log("\n=== PHASE 1: Create extreme scenario ===");
uint256 traderBalanceBefore = weth.balanceOf(account);
console.log("Trader balance before:", vm.toString(traderBalanceBefore));
buy(200 ether);
// Check state after extreme buy
uint256 postBuyEthBalance = address(lm).balance + weth.balanceOf(address(lm));
uint256 postBuyOutstandingHarb = harberg.outstandingSupply();
(, int24 postBuyTick, , , , , ) = pool.slot0();
(, int24 postBuyTick,,,,,) = pool.slot0();
console.log("\n=== POST-BUY STATE ===");
console.log("LM ETH balance:", vm.toString(postBuyEthBalance));
console.log("Outstanding HARB:", vm.toString(postBuyOutstandingHarb));
console.log("Current tick:", vm.toString(postBuyTick));
console.log("ETH/HARB ratio:", vm.toString(postBuyEthBalance * 1e18 / postBuyOutstandingHarb));
// Diagnose the scenario type
console.log("\n=== SCENARIO DIAGNOSIS ===");
if (postBuyTick >= TickMath.MAX_TICK - EXTREME_PRICE_MARGIN) {
@ -731,26 +744,26 @@ contract LiquidityManagerTest is UniswapTestBase {
} else {
console.log("[DIAGNOSIS] NORMAL RANGE - may still have arithmetic issues");
}
if (postBuyEthBalance < postBuyOutstandingHarb / 1000) {
console.log("[WARNING] PROTOCOL DEATH RISK - insufficient ETH reserves");
} else {
console.log("[DIAGNOSIS] ADEQUATE RESERVES - arithmetic overflow if any");
}
// Test the intelligent recenter with diagnostics
console.log("\n=== PHASE 2: Test intelligent recenter ===");
recenter(false);
// Check final state
(, int24 finalTick, , , , , ) = pool.slot0();
(, int24 finalTick,,,,,) = pool.slot0();
console.log("\n=== FINAL STATE ===");
console.log("Final tick:", vm.toString(finalTick));
console.log("[SUCCESS] Test completed successfully");
// Test passes if we reach here without reverting
}
/// @notice Executes a single random trade based on available balances
/// @param amount Base amount for trade calculations
/// @param harbergBal Current HARB balance of the account
@ -773,7 +786,7 @@ contract LiquidityManagerTest is UniswapTestBase {
}
}
}
/// @notice Calculates appropriate buy amount based on available WETH
/// @param baseAmount Base amount for calculation
/// @return Calculated buy amount bounded by available WETH
@ -786,11 +799,11 @@ contract LiquidityManagerTest is UniswapTestBase {
// ========================================
// ROBUSTNESS AND FUZZ TESTS
// ========================================
/// @notice Fuzz test to ensure protocol robustness under random trading sequences
/// @dev Validates that traders cannot extract value through arbitrary trading patterns
/// This is a pure unit test with no CSV recording or scenario analysis
/// @param numActions Number of buy/sell operations to perform
/// @param numActions Number of buy/sell operations to perform
/// @param frequency How often to trigger recentering operations
/// @param amounts Array of trade amounts to use (bounded automatically)
function testFuzzRobustness(uint8 numActions, uint8 frequency, uint8[] calldata amounts) public {
@ -801,41 +814,43 @@ contract LiquidityManagerTest is UniswapTestBase {
_setupCustom(numActions % 2 == 0 ? true : false, 20 ether);
uint256 traderBalanceBefore = weth.balanceOf(account);
// Execute random trading sequence
_executeRandomTradingSequence(numActions, frequency, amounts);
uint256 traderBalanceAfter = weth.balanceOf(account);
// Core unit test assertion: protocol should not allow trader profit
assertGe(traderBalanceBefore, traderBalanceAfter, "Protocol must prevent trader profit through arbitrary trading");
assertGe(
traderBalanceBefore, traderBalanceAfter, "Protocol must prevent trader profit through arbitrary trading"
);
}
/// @notice Helper to execute a sequence of random trades and recentering
/// @dev Extracted for reuse in both unit tests and scenario analysis
function _executeRandomTradingSequence(uint8 numActions, uint8 frequency, uint8[] calldata amounts) internal {
uint8 recenterFrequencyCounter = 0;
for (uint i = 0; i < numActions; i++) {
for (uint256 i = 0; i < numActions; i++) {
uint256 amount = (uint256(amounts[i]) * MIN_TRADE_AMOUNT) + MIN_TRADE_AMOUNT;
uint256 harbergBal = harberg.balanceOf(account);
// Execute trade based on current balances and random input
_executeRandomTrade(amount, harbergBal);
// Handle extreme price conditions to prevent test failures
(, int24 currentTick, , , , , ) = pool.slot0();
(, int24 currentTick,,,,,) = pool.slot0();
if (currentTick < -887270) {
// Price too low - small buy to stabilize
uint256 wethBal = weth.balanceOf(account);
if (wethBal > 0) buy(wethBal / 100);
}
if (currentTick > 887270) {
// Price too high - small sell to stabilize
// Price too high - small sell to stabilize
uint256 harbBal = harberg.balanceOf(account);
if (harbBal > 0) sell(harbBal / 100);
}
// Periodic recentering based on frequency
if (recenterFrequencyCounter >= frequency) {
recenter(false);
@ -853,4 +868,131 @@ contract LiquidityManagerTest is UniswapTestBase {
recenter(true);
}
// ========================================
// VWAP INTEGRATION VALIDATION TESTS
// ========================================
/// @notice Tests VWAP system integration and behavioral correctness
/// @dev Validates VWAP accumulation, floor positioning, and system stability across trading sequences
function testVWAPIntegrationValidation() public {
// Setup with known initial conditions
_setupCustom(false, 100 ether);
// Record initial state - should be zero volume
assertEq(lm.cumulativeVolumeWeightedPriceX96(), 0, "Initial VWAP should be zero");
assertEq(lm.cumulativeVolume(), 0, "Initial volume should be zero");
// Execute first trade and recenter to trigger VWAP recording
buy(10 ether);
recenter(false);
// Check VWAP after first trade
uint256 vwapAfterFirst = lm.cumulativeVolumeWeightedPriceX96();
uint256 volumeAfterFirst = lm.cumulativeVolume();
assertGt(vwapAfterFirst, 0, "VWAP should be recorded after first trade");
assertGt(volumeAfterFirst, 0, "Volume should be recorded after first trade");
// Calculate first VWAP
uint256 firstCalculatedVWAP = vwapAfterFirst / volumeAfterFirst;
assertGt(firstCalculatedVWAP, 0, "VWAP should be positive");
assertLt(firstCalculatedVWAP, type(uint128).max, "VWAP should be reasonable");
// Execute larger second trade to ensure price movement and recenter triggers
buy(15 ether);
recenter(false);
// Check VWAP after second trade
uint256 vwapAfterSecond = lm.cumulativeVolumeWeightedPriceX96();
uint256 volumeAfterSecond = lm.cumulativeVolume();
assertGt(vwapAfterSecond, vwapAfterFirst, "Cumulative VWAP should increase after second trade");
assertGt(volumeAfterSecond, volumeAfterFirst, "Cumulative volume should increase after second trade");
// Calculate final VWAP
uint256 finalCalculatedVWAP = vwapAfterSecond / volumeAfterSecond;
// Verify VWAP is reasonable and accumulating correctly
assertGt(finalCalculatedVWAP, 0, "Final VWAP should be positive");
assertLt(finalCalculatedVWAP, type(uint128).max, "Final VWAP should be reasonable");
assertGt(finalCalculatedVWAP, firstCalculatedVWAP / 100, "Final VWAP should be in similar magnitude as first");
assertLt(finalCalculatedVWAP, firstCalculatedVWAP * 100, "Final VWAP should be in similar magnitude as first");
console.log("=== VWAP Calculation Test Results ===");
console.log("Final VWAP:", vm.toString(finalCalculatedVWAP >> 32));
console.log("Total volume:", vm.toString(volumeAfterSecond));
// Verify VWAP is being used for floor position
_verifyFloorUsesVWAP(finalCalculatedVWAP);
}
/// @notice Helper function to get current price in X96 format
/// @return priceX96 Current price in X96 format
function _getCurrentPriceX96() internal view returns (uint256 priceX96) {
(uint160 sqrtPriceX96,,,,,,) = pool.slot0();
priceX96 = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) >> 96;
}
/// @notice Helper function to verify floor position uses VWAP
function _verifyFloorUsesVWAP(uint256 /* expectedVWAP */ ) internal view {
// Get floor position details
(uint128 floorLiquidity, int24 floorTickLower, int24 floorTickUpper) =
lm.positions(LiquidityManager.Stage.FLOOR);
assertGt(floorLiquidity, 0, "Floor position should have liquidity");
// Calculate the midpoint of floor position
int24 floorMidTick = floorTickLower + (floorTickUpper - floorTickLower) / 2;
// Get current tick for comparison
(, int24 currentTick,,,,,) = pool.slot0();
// Floor position should be meaningfully different from current tick (using VWAP)
// Since we bought HARB, current price moved up, but floor should be positioned
// at a discounted VWAP level (70% of VWAP + capital inefficiency adjustment)
int24 tickDifference = currentTick - floorMidTick;
// The floor should be positioned at a discounted level compared to current price
// Since we bought HARB (price went up), the floor should be at a lower price level
// Let's debug the actual tick relationship first
console.log("Token0 is WETH:", token0isWeth);
console.log("Floor mid-tick:", vm.toString(floorMidTick));
console.log("Current tick:", vm.toString(currentTick));
console.log("Tick difference (current - floor):", vm.toString(tickDifference));
// The floor should be meaningfully different from current tick (using historical VWAP)
// Since we executed trades that moved price up, floor should be positioned differently
int24 absDifference = tickDifference < 0 ? -tickDifference : tickDifference;
assertGt(absDifference, 50, "Floor should be positioned meaningfully away from current price");
// Based on the actual behavior observed:
// - We bought HARB, so current price moved up (current tick = -113852)
// - Floor is positioned at -176700 (much lower tick)
// - Difference is 62848 (positive, meaning current > floor in tick terms)
// In HARB/WETH pair where HARB is token0:
// - Lower tick numbers = higher HARB price (more WETH per HARB)
// - Higher tick numbers = lower HARB price (less WETH per HARB)
// The floor being at a lower tick (-176700) means it's positioned for higher HARB prices
// This makes sense because floor position provides ETH liquidity to buy back HARB
// when HARB price falls. So it's positioned above current price as a "floor support"
// Verify that floor is positioned meaningfully different from current price
// and that the difference makes economic sense (floor supports higher HARB prices)
if (!token0isWeth) {
// HARB is token0: floor should be at lower tick (higher HARB price) than current
assertGt(tickDifference, 0, "Floor should be positioned to support higher HARB prices");
assertGt(tickDifference, 1000, "Floor should be meaningfully positioned for price support");
} else {
// WETH is token0: floor should be at higher tick (lower HARB price) than current
assertLt(tickDifference, 0, "Floor should be positioned below current HARB price");
assertLt(tickDifference, -1000, "Floor should be meaningfully positioned for price support");
}
// Verify the tick difference is reasonable (not extreme)
assertLt(absDifference, 100000, "Floor position should not be extremely far from current price");
console.log("Floor positioned at discounted VWAP level - PASS");
}
}

View file

@ -12,10 +12,11 @@ contract StakeTest is Test {
address liquidityPool;
address liquidityManager;
event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 harbergDeposit, uint256 share, uint32 taxRate);
event PositionCreated(
uint256 indexed positionId, address indexed owner, uint256 harbergDeposit, uint256 share, uint32 taxRate
);
event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 harbergPayout);
function setUp() public {
harberg = new Harberg("HARB", "HARB");
stakingPool = new Stake(address(harberg), makeAddr("taxRecipient"));
@ -25,13 +26,13 @@ contract StakeTest is Test {
}
function assertPosition(uint256 positionId, uint256 expectedShares, uint32 expectedTaxRate) private view {
(uint256 shares, , , , uint32 taxRate) = stakingPool.positions(positionId);
(uint256 shares,,,, uint32 taxRate) = stakingPool.positions(positionId);
assertEq(shares, expectedShares, "Incorrect share amount for new position");
assertEq(taxRate, expectedTaxRate, "Incorrect tax rate for new position");
}
function verifyPositionShrunkOrRemoved(uint256 positionId, uint256 initialStake) private view {
(uint256 remainingShare, , , , ) = stakingPool.positions(positionId);
(uint256 remainingShare,,,,) = stakingPool.positions(positionId);
uint256 expectedInitialShares = stakingPool.assetsToShares(initialStake);
bool positionRemoved = remainingShare == 0;
bool positionShrunk = remainingShare < expectedInitialShares;
@ -60,8 +61,12 @@ contract StakeTest is Test {
uint256 positionId = stakingPool.snatch(stakeAmount, staker, 1, empty);
// Check results
assertEq(stakingPool.outstandingStake(), stakingPool.assetsToShares(stakeAmount), "Outstanding stake did not update correctly");
(uint256 share, address owner, uint32 creationTime, , uint32 taxRate) = stakingPool.positions(positionId);
assertEq(
stakingPool.outstandingStake(),
stakingPool.assetsToShares(stakeAmount),
"Outstanding stake did not update correctly"
);
(uint256 share, address owner, uint32 creationTime,, uint32 taxRate) = stakingPool.positions(positionId);
assertEq(stakingPool.sharesToAssets(share), stakeAmount, "Stake amount in position is incorrect");
assertEq(owner, staker, "Stake owner is incorrect");
assertEq(creationTime, uint32(block.timestamp), "Creation time is incorrect");
@ -76,7 +81,7 @@ contract StakeTest is Test {
address staker = makeAddr("staker");
vm.startPrank(liquidityManager);
harberg.mint(stakeAmount * 5); // Ensuring the staker has enough balance
harberg.mint(stakeAmount * 5); // Ensuring the staker has enough balance
harberg.transfer(staker, stakeAmount);
vm.stopPrank();
@ -105,7 +110,7 @@ contract StakeTest is Test {
assertEq(stakingPool.outstandingStake(), 0, "Outstanding stake not updated correctly");
// Ensure the position is cleared
(, address owner, uint32 time, , ) = stakingPool.positions(positionId);
(, address owner, uint32 time,,) = stakingPool.positions(positionId);
assertEq(time, 0, "Position time not cleared");
assertEq(owner, address(0), "Position owner not cleared");
@ -166,7 +171,7 @@ contract StakeTest is Test {
}
function denormTR(uint256 normalizedTaxRate) internal pure returns (uint256) {
return normalizedTaxRate * 97;
return normalizedTaxRate * 97;
}
function testAvgTaxRateAndPercentageStaked() public {
@ -177,9 +182,9 @@ contract StakeTest is Test {
// Mint and distribute tokens
vm.startPrank(liquidityManager);
// mint all the tokens we will need in the test
// mint all the tokens we will need in the test
harberg.mint((smallstake + stakeOneThird + stakeTwoThird) * 5);
// send 20% of that to staker
// send 20% of that to staker
harberg.transfer(staker, (smallstake + stakeOneThird + stakeTwoThird) * 2);
vm.stopPrank();
@ -187,11 +192,11 @@ contract StakeTest is Test {
uint256 positionId1 = doSnatch(staker, smallstake, 0);
uint256 avgTaxRate;
uint256 percentageStaked;
uint256 percentageStaked;
avgTaxRate = stakingPool.getAverageTaxRate();
percentageStaked = stakingPool.getPercentageStaked();
percentageStaked = stakingPool.getPercentageStaked();
// let this be about 10 basis points of tax rate
// let this be about 10 basis points of tax rate
assertApproxEqRel(bp(denormTR(avgTaxRate)), 10, 1e17);
assertApproxEqRel(bp(percentageStaked), 10, 1e17);
@ -200,7 +205,7 @@ contract StakeTest is Test {
uint256 positionId2 = doSnatch(staker, stakeOneThird, 2);
avgTaxRate = stakingPool.getAverageTaxRate();
percentageStaked = stakingPool.getPercentageStaked();
percentageStaked = stakingPool.getPercentageStaked();
assertApproxEqRel(bp(denormTR(avgTaxRate)), 50, 1e17);
assertApproxEqRel(bp(percentageStaked), 300, 1e17);
@ -211,7 +216,7 @@ contract StakeTest is Test {
positionId2 = doSnatch(staker, stakeTwoThird, 11);
avgTaxRate = stakingPool.getAverageTaxRate();
percentageStaked = stakingPool.getPercentageStaked();
percentageStaked = stakingPool.getPercentageStaked();
assertApproxEqRel(bp(denormTR(avgTaxRate)), 730, 1e17);
assertApproxEqRel(bp(percentageStaked), 1000, 1e17);
@ -233,7 +238,7 @@ contract StakeTest is Test {
positionId2 = doSnatch(staker, stakeTwoThird, 15);
avgTaxRate = stakingPool.getAverageTaxRate();
percentageStaked = stakingPool.getPercentageStaked();
percentageStaked = stakingPool.getPercentageStaked();
assertApproxEqRel(bp(denormTR(avgTaxRate)), 2500, 1e17);
assertApproxEqRel(bp(percentageStaked), 660, 1e17);
@ -245,18 +250,17 @@ contract StakeTest is Test {
positionId1 = doSnatch(staker, stakeOneThird, 15);
avgTaxRate = stakingPool.getAverageTaxRate();
percentageStaked = stakingPool.getPercentageStaked();
percentageStaked = stakingPool.getPercentageStaked();
assertApproxEqRel(bp(denormTR(avgTaxRate)), 2500, 1e17);
assertApproxEqRel(bp(percentageStaked), 330, 1e17);
}
function testRevert_SharesTooLow() public {
address staker = makeAddr("staker");
vm.startPrank(liquidityManager);
harberg.mint(10 ether);
uint256 tooSmallStake = harberg.previousTotalSupply() / 4000; // Less than minStake calculation
uint256 tooSmallStake = harberg.previousTotalSupply() / 4000; // Less than minStake calculation
harberg.transfer(staker, tooSmallStake);
vm.stopPrank();
@ -264,7 +268,11 @@ contract StakeTest is Test {
harberg.approve(address(stakingPool), tooSmallStake);
uint256[] memory empty;
vm.expectRevert(abi.encodeWithSelector(Stake.StakeTooLow.selector, staker, tooSmallStake, harberg.previousTotalSupply() / 3000));
vm.expectRevert(
abi.encodeWithSelector(
Stake.StakeTooLow.selector, staker, tooSmallStake, harberg.previousTotalSupply() / 3000
)
);
stakingPool.snatch(tooSmallStake, staker, 1, empty);
vm.stopPrank();
}
@ -285,10 +293,10 @@ contract StakeTest is Test {
harberg.approve(address(stakingPool), 1 ether);
uint256[] memory positions = new uint256[](1);
positions[0] = positionId; // Assuming position ID 1 has tax rate 5
positions[0] = positionId; // Assuming position ID 1 has tax rate 5
vm.expectRevert(abi.encodeWithSelector(Stake.TaxTooLow.selector, newStaker, 5, 5, positionId));
stakingPool.snatch(1 ether, newStaker, 5, positions); // Same tax rate should fail
stakingPool.snatch(1 ether, newStaker, 5, positions); // Same tax rate should fail
vm.stopPrank();
}
@ -309,7 +317,9 @@ contract StakeTest is Test {
uint256[] memory positions = new uint256[](1);
positions[0] = positionId;
vm.expectRevert(abi.encodeWithSelector(TooMuchSnatch.selector, ambitiousStaker, 500000 ether, 1000000 ether, 1000000 ether));
vm.expectRevert(
abi.encodeWithSelector(TooMuchSnatch.selector, ambitiousStaker, 500000 ether, 1000000 ether, 1000000 ether)
);
stakingPool.snatch(1 ether, ambitiousStaker, 20, positions);
vm.stopPrank();
}
@ -356,7 +366,7 @@ contract StakeTest is Test {
vm.stopPrank();
// Verify the change
(, , , , uint32 taxRate) = stakingPool.positions(positionId);
(,,,, uint32 taxRate) = stakingPool.positions(positionId);
assertEq(taxRate, newTaxRate, "Tax rate did not update correctly");
// notOwner tries to change tax rate
@ -377,14 +387,13 @@ contract StakeTest is Test {
vm.startPrank(staker);
harberg.approve(address(stakingPool), 1 ether);
uint256[] memory empty;
uint256 positionId = stakingPool.snatch(1 ether, staker, 5, empty); // Using tax rate index 5, which is 18% per year
(uint256 shareBefore, , , , ) = stakingPool.positions(positionId);
uint256 positionId = stakingPool.snatch(1 ether, staker, 5, empty); // Using tax rate index 5, which is 18% per year
(uint256 shareBefore,,,,) = stakingPool.positions(positionId);
// Immediately after staking, no tax due
stakingPool.payTax(positionId);
// Verify no change in position
(uint256 share, , , , ) = stakingPool.positions(positionId);
(uint256 share,,,,) = stakingPool.positions(positionId);
assertEq(share, shareBefore, "Share should not change when no tax is due");
// Move time forward 30 days
@ -393,7 +402,7 @@ contract StakeTest is Test {
vm.stopPrank();
// Check that the tax was paid and position updated
(share, , , , ) = stakingPool.positions(positionId);
(share,,,,) = stakingPool.positions(positionId);
uint256 daysElapsed = 30;
uint256 taxRate = 18; // Corresponding to 18% annually
uint256 daysInYear = 365;
@ -417,14 +426,13 @@ contract StakeTest is Test {
vm.startPrank(staker);
harberg.approve(address(stakingPool), 1 ether);
uint256[] memory empty;
uint256 positionId = stakingPool.snatch(1 ether, staker, 12, empty); // Using tax rate index 5, which is 100% per year
uint256 positionId = stakingPool.snatch(1 ether, staker, 12, empty); // Using tax rate index 5, which is 100% per year
vm.warp(block.timestamp + 365 days); // Move time forward to ensure maximum tax due
stakingPool.payTax(positionId);
vm.stopPrank();
// Verify position is liquidated
(uint256 share, , , , ) = stakingPool.positions(positionId);
(uint256 share,,,,) = stakingPool.positions(positionId);
assertEq(share, 0, "Share should be zero after liquidation");
}
}

View file

@ -11,7 +11,8 @@ library CSVHelper {
* @return The CSV header as a string.
*/
function createPositionsHeader() internal pure returns (string memory) {
return "precedingAction, currentTick, floorTickLower, floorTickUpper, floorEth, floorHarb, anchorTickLower, anchorTickUpper, anchorEth, anchorHarb, discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryHarb";
return
"precedingAction, currentTick, floorTickLower, floorTickUpper, floorEth, floorHarb, anchorTickLower, anchorTickUpper, anchorEth, anchorHarb, discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryHarb";
}
function createTimeSeriesHeader() internal pure returns (string memory) {
@ -83,7 +84,7 @@ library CSVHelper {
absValue /= 10;
}
if (negative) {
bstr[0] = '-';
bstr[0] = "-";
}
return string(bstr);
}

View file

@ -7,7 +7,6 @@ import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
import "../../src/interfaces/IWETH9.sol";
import {Harberg} from "../../src/Harberg.sol";
/**
* @title UniswapTestBase
* @dev Base contract for Uniswap V3 testing, providing reusable swap logic.
@ -40,7 +39,7 @@ abstract contract UniswapTestBase is Test {
// Set the sqrtPriceLimitX96 based on the swap direction
// Get current price to set appropriate limits
(uint160 currentSqrtPrice,,,,,,) = pool.slot0();
if (zeroForOne) {
// Swapping token0 for token1 - price goes down
// sqrtPriceLimitX96 must be less than current price but greater than MIN_SQRT_RATIO
@ -58,30 +57,19 @@ abstract contract UniswapTestBase is Test {
}
}
pool.swap(
account,
zeroForOne,
int256(amount),
limit,
abi.encode(account, int256(amount), isBuy)
);
pool.swap(account, zeroForOne, int256(amount), limit, abi.encode(account, int256(amount), isBuy));
}
/**
* @dev The Uniswap V3 swap callback.
*/
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata _data
) external {
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) external {
require(amount0Delta > 0 || amount1Delta > 0);
(address seller, , bool isBuy) = abi.decode(_data, (address, uint256, bool));
(address seller,, bool isBuy) = abi.decode(_data, (address, uint256, bool));
(, uint256 amountToPay) = amount0Delta > 0
? (!token0isWeth, uint256(amount0Delta))
: (token0isWeth, uint256(amount1Delta));
(, uint256 amountToPay) =
amount0Delta > 0 ? (!token0isWeth, uint256(amount0Delta)) : (token0isWeth, uint256(amount1Delta));
if (isBuy) {
weth.transfer(msg.sender, amountToPay);
} else {
@ -101,7 +89,7 @@ abstract contract UniswapTestBase is Test {
harberg.mint(harbPulled);
harberg.transfer(msg.sender, harbPulled);
}
// pack ETH
uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed;
if (weth.balanceOf(address(this)) < ethOwed) {

View file

@ -1,4 +1,3 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
@ -8,29 +7,28 @@ import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol";
contract MockOptimizer is Initializable, UUPSUpgradeable {
Harberg private harberg;
Stake private stake;
Harberg private harberg;
Stake private stake;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error UnauthorizedAccount(address account);
function initialize(address _harberg, address _stake) initializer public {
_changeAdmin(msg.sender);
harberg = Harberg(_harberg);
stake = Stake(_stake);
function initialize(address _harberg, address _stake) public initializer {
_changeAdmin(msg.sender);
harberg = Harberg(_harberg);
stake = Stake(_stake);
}
/**
* @dev Throws if called by any account other than the admin.
*/
modifier onlyAdmin() {
_checkAdmin();
_;
}
/**
* @dev Throws if the sender is not the admin.
*/
@ -39,41 +37,34 @@ contract MockOptimizer is Initializable, UUPSUpgradeable {
revert UnauthorizedAccount(msg.sender);
}
}
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {}
function calculateSentiment(uint256, uint256) public pure returns (uint256 sentimentValue) {
return 0;
}
/// @notice Computes the staker sentiment based on the proportion of the authorized stake that is currently staked.
/// @return sentiment A number between 0 and 200 indicating the market sentiment.
function getSentiment() external view returns (uint256 sentiment) {
uint256 percentageStaked = stake.getPercentageStaked();
uint256 averageTaxRate = stake.getAverageTaxRate();
uint256 averageTaxRate = stake.getAverageTaxRate();
sentiment = calculateSentiment(averageTaxRate, percentageStaked);
}
/// @notice Returns mock liquidity parameters for testing
/// @return capitalInefficiency Mock capital inefficiency (50%)
/// @return anchorShare Mock anchor share (50%)
/// @return anchorShare Mock anchor share (50%)
/// @return anchorWidth Mock anchor width (50)
/// @return discoveryDepth Mock discovery depth (50%)
function getLiquidityParams()
external
pure
returns (
uint256 capitalInefficiency,
uint256 anchorShare,
uint24 anchorWidth,
uint256 discoveryDepth
)
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
capitalInefficiency = 5*10**17; // 50%
anchorShare = 5*10**17; // 50%
anchorWidth = 50; // 50
discoveryDepth = 5*10**17; // 50%
capitalInefficiency = 5 * 10 ** 17; // 50%
anchorShare = 5 * 10 ** 17; // 50%
anchorWidth = 50; // 50
discoveryDepth = 5 * 10 ** 17; // 50%
}
}