added overflow checks

This commit is contained in:
JulesCrown 2024-07-18 07:35:39 +02:00
parent dbc23802d2
commit b243874f02
7 changed files with 493 additions and 327 deletions

View file

@ -4,7 +4,7 @@ import "forge-std/Script.sol";
import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "../src/Harb.sol";
import "../src/Harberg.sol";
import "../src/Stake.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
@ -60,7 +60,7 @@ contract SepoliaScript is Script {
TwabController tc = TwabController(TWABC);
// in case you want to deploy an new TwabController
//TwabController tc = new TwabController(60 * 60, uint32(block.timestamp));
Harb harb = new Harb("Harberger Tax", "HARB", tc);
Harberg harb = new Harberg("Harbergerger Tax", "HARB", tc);
token0isWeth = address(WETH) < address(harb);
Stake stake = new Stake(address(harb));
harb.setStakingPool(address(stake));

View file

@ -1,72 +1,78 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import {ERC20, IERC20, IERC20Metadata} from "@openzeppelin/token/ERC20/ERC20.sol";
import {ERC20Permit, IERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
import {SafeCast} from "@openzeppelin/utils/math/SafeCast.sol";
import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
import {Math} from "@openzeppelin/utils/math/Math.sol";
import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
/**
* @title Harb ERC20 Token
* @notice Implements an ERC20 token with mechanisms for minting, burning, and transferring tokens,
* integrated with a TWAB controller for time-weighted balance tracking. This token supports
* a novel economic model that finances Universal Basic Income (UBI) through a Harberger tax,
* applied to staked tokens. The liquidityManager manages token supply to stabilize market liquidity.
* @title Harberg ERC20 Token
* @notice This contract implements an ERC20 token with mechanisms for minting, burning, and transferring tokens,
* integrated with a TWAB controller for time-weighted balance tracking. This token supports a novel economic model
* that finances Universal Basic Income (UBI) through a Harberger tax applied to staked tokens. The liquidity manager
* manages token supply to stabilize market liquidity.
*/
contract Harb is ERC20, ERC20Permit {
contract Harberg is ERC20, ERC20Permit {
using Math for uint256;
// only working with UNI V3 1% fee tier pools
// Total tax collected so far
uint256 public sumTaxCollected;
// Constant for UNI V3 1% fee tier pools
uint24 private constant FEE = uint24(10_000);
// to prevent framentation of staking positions, a minimum size of the stake is introduced.
// snatching 600 positions will blow block gas limit, 20% of supply can be staked so 5 * 600 is a good start
uint256 MIN_STAKE_FRACTION = 3000;
// later it will keep small stakers out, so let's have positions shrink, balancing the risk of high gas cost.
uint256 MAX_STAKE_FRACTION = 100000;
// from PoolTogether: the beginning timestamp for the first period. This allows us to maximize storage as well as line up periods with a chosen timestamp.
// Minimum fraction of the total supply required for staking to prevent fragmentation of staking positions
uint256 private constant MIN_STAKE_FRACTION = 3000;
// Maximum fraction of the total supply allowed for staking to manage gas costs
uint256 private constant MAX_STAKE_FRACTION = 100000;
// Period offset for TWAB calculations
uint256 private immutable PERIOD_OFFSET;
// from PoolTogether: the minimum period length for Observations. When a period elapses, a new Observation is recorded, otherwise the most recent Observation is updated.
// Minimum period length for TWAB observations
uint256 private immutable PERIOD_LENGTH;
//periphery contracts
// Immutable reference to the TWAB controller
TwabController private immutable twabController;
// Address of the liquidity manager
address private liquidityManager;
// Address of the staking pool
address private stakingPool;
// Address of the liquidity pool
address private liquidityPool;
// Address of the tax pool
address public constant TAX_POOL = address(2);
/* ============ Public Variables ============ */
uint256 public sumTaxCollected;
// Previous total supply for staking calculations
uint256 public previousTotalSupply;
// Minimum fraction of the total supply required for staking
uint256 public minStakeSupplyFraction;
// Structure to hold UBI title information for each account
struct UbiTitle {
uint256 sumTaxCollected;
uint256 time;
}
// Mapping to store UBI titles for each account
mapping(address => UbiTitle) public ubiTitles;
/* ============ Errors ============ */
// Custom errors
error ZeroAddressInConstructor();
error ZeroAddressInSetter();
error AddressAlreadySet();
// Event emitted when UBI is claimed
event UbiClaimed(address indexed owner, uint256 ubiAmount);
/// @dev Function modifier to ensure that the caller is the liquidityManager
// Modifier to restrict access to the liquidity manager
modifier onlyLiquidityManager() {
require(msg.sender == address(liquidityManager), "Harb/only-lm");
require(msg.sender == address(liquidityManager), "Harberg/only-lm");
_;
}
/**
* @notice TwabERC20 Constructor
* @notice Constructor for the Harberg token
* @param name_ The name of the token
* @param symbol_ The token symbol
* @param symbol_ The symbol of the token
* @param twabController_ The TWAB controller contract
*/
constructor(string memory name_, string memory symbol_, TwabController twabController_)
ERC20(name_, symbol_)
@ -79,48 +85,64 @@ contract Harb is ERC20, ERC20Permit {
minStakeSupplyFraction = MIN_STAKE_FRACTION;
}
/// @notice Sets the address for the liquidityPool. Used once post-deployment to initialize the contract.
/// @dev Should be called only once right after the contract deployment to set the liquidity pool address.
/// Throws AddressAlreadySet if called more than once.
/// @param liquidityPool_ The address of the liquidity pool.
/**
* @notice Sets the address for the liquidityPool. Used once post-deployment to initialize the contract.
* @dev Should be called only once right after the contract deployment to set the liquidity pool address.
* Throws AddressAlreadySet if called more than once.
* @param liquidityPool_ The address of the liquidity pool.
*/
function setLiquidityPool(address liquidityPool_) external {
if (address(0) == liquidityPool_) revert ZeroAddressInSetter();
if (liquidityPool != address(0)) revert AddressAlreadySet();
liquidityPool = liquidityPool_;
}
/// @notice Sets the address for the liquidityManager. Used once post-deployment to initialize the contract.
/// @dev Should be called only once right after the contract deployment to set the liquidity manager address.
/// Throws AddressAlreadySet if called more than once.
/// @param liquidityManager_ The address of the liquidity manager.
/**
* @notice Sets the address for the liquidityManager. Used once post-deployment to initialize the contract.
* @dev Should be called only once right after the contract deployment to set the liquidity manager address.
* Throws AddressAlreadySet if called more than once.
* @param liquidityManager_ The address of the liquidity manager.
*/
function setLiquidityManager(address liquidityManager_) external {
if (address(0) == liquidityManager_) revert ZeroAddressInSetter();
if (liquidityManager != address(0)) revert AddressAlreadySet();
liquidityManager = liquidityManager_;
}
/// @notice Sets the address for the stakingPool. Used once post-deployment to initialize the contract.
/// @dev Should be called only once right after the contract deployment to set the staking pool address.
/// Throws AddressAlreadySet if called more than once.
/// @param stakingPool_ The address of the staking pool.
/**
* @notice Sets the address for the stakingPool. Used once post-deployment to initialize the contract.
* @dev Should be called only once right after the contract deployment to set the staking pool address.
* Throws AddressAlreadySet if called more than once.
* @param stakingPool_ The address of the staking pool.
*/
function setStakingPool(address stakingPool_) external {
if (address(0) == stakingPool_) revert ZeroAddressInSetter();
if (stakingPool != address(0)) revert AddressAlreadySet();
stakingPool = stakingPool_;
}
function getPeripheryContracts() external view returns (address, address, address, address) {
/**
* @notice Returns the addresses of the periphery contracts
* @return The addresses of the TWAB controller, liquidity manager, staking pool, and liquidity pool
*/
function peripheryContracts() external view returns (address, address, address, address) {
return (address(twabController), liquidityManager, stakingPool, liquidityPool);
}
/**
* @notice Calculates the minimum stake based on the previous total supply
* @return The minimum stake amount
*/
function minStake() external view returns (uint256) {
return previousTotalSupply / minStakeSupplyFraction;
}
/// @notice Allows the liquidityManager to mint tokens for itself.
/// @dev Tokens minted are managed as community liquidity in the Uniswap pool to stabilize HARB prices.
/// Only callable by the Liquidity Manager. Minting rules and limits are defined externally.
/// @param _amount The number of tokens to mint.
/**
* @notice Allows the liquidity manager to mint tokens for itself.
* @dev Tokens minted are managed as community liquidity in the Uniswap pool to stabilize HARB prices.
* Only callable by the Liquidity Manager. Minting rules and limits are defined externally.
* @param _amount The number of tokens to mint.
*/
function mint(uint256 _amount) external onlyLiquidityManager {
_mint(address(liquidityManager), _amount);
if (previousTotalSupply == 0) {
@ -128,18 +150,28 @@ contract Harb is ERC20, ERC20Permit {
}
}
/// @notice Allows the liquidityManager to burn tokens from its account, adjusting the staking pool accordingly.
/// @dev When tokens are burned, the total supply shrinks, making excess tokens in the staking pool unnecessary.
/// These excess tokens are burned to maintain the guaranteed fixed percentage of the total supply for stakers.
/// @param _amount The number of tokens to burn.
/**
* @notice Allows the liquidity manager to burn tokens from its account, adjusting the staking pool accordingly.
* @dev When tokens are burned, the total supply shrinks, making excess tokens in the staking pool unnecessary.
* These excess tokens are burned to maintain the guaranteed fixed percentage of the total supply for stakers.
* @param _amount The number of tokens to burn.
*/
function burn(uint256 _amount) external onlyLiquidityManager {
_burn(address(liquidityManager), _amount);
}
/**
* @notice Sets the previous total supply
* @param _ts The previous total supply value
*/
function setPreviousTotalSupply(uint256 _ts) external onlyLiquidityManager {
previousTotalSupply = _ts;
}
/**
* @notice Sets the minimum stake supply fraction
* @param _mssf The minimum stake supply fraction value
*/
function setMinStakeSupplyFraction(uint256 _mssf) external onlyLiquidityManager {
require(_mssf >= MIN_STAKE_FRACTION, "minStakeSupplyFraction below allowed min");
require(_mssf <= MAX_STAKE_FRACTION, "minStakeSupplyFraction above allow max");
@ -148,16 +180,24 @@ contract Harb is ERC20, ERC20Permit {
/* ============ Public ERC20 Overrides ============ */
/// @inheritdoc ERC20
/**
* @inheritdoc ERC20
*/
function balanceOf(address _account) public view override(ERC20) returns (uint256) {
return twabController.balanceOf(address(this), _account);
}
/// @inheritdoc ERC20
/**
* @inheritdoc ERC20
*/
function totalSupply() public view override(ERC20) returns (uint256) {
return twabController.totalSupply(address(this));
}
/**
* @notice Returns the outstanding supply, excluding the balances of the liquidity pool and liquidity manager
* @return The outstanding supply
*/
function outstandingSupply() public view returns (uint256) {
return totalSupply() - balanceOf(liquidityPool) - balanceOf(liquidityManager);
}
@ -191,24 +231,23 @@ contract Harb is ERC20, ERC20Permit {
}
/**
* @notice Destroys tokens from `_owner` and reduces the total supply.
* @notice Burns tokens from `_owner` and decreases the total supply.
* @dev Emits a {Transfer} event with `to` set to the zero address.
* @dev `_owner` cannot be the zero address.
* @dev `_owner` must have at least `_amount` tokens.
* @param _owner The owner of the tokens
* @param _amount The amount of tokens to burn
* @param owner Address that will have tokens burnt
* @param amount Tokens to burn
*/
function _burn(address _owner, uint256 _amount) internal override {
if (_amount > 0) {
function _burn(address owner, uint256 amount) internal override {
if (amount > 0) {
// shrink staking pool proportional to economy
uint256 stakingPoolBalance = balanceOf(stakingPool);
if (stakingPoolBalance > 0) {
uint256 excessStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance);
uint256 excessStake = stakingPoolBalance * amount / (totalSupply() - stakingPoolBalance);
twabController.burn(stakingPool, SafeCast.toUint96(excessStake));
emit Transfer(stakingPool, address(0), excessStake);
}
twabController.burn(_owner, SafeCast.toUint96(_amount));
emit Transfer(_owner, address(0), _amount);
twabController.burn(owner, SafeCast.toUint96(amount));
emit Transfer(owner, address(0), amount);
}
}
@ -224,7 +263,9 @@ contract Harb is ERC20, ERC20Permit {
*/
function _transfer(address _from, address _to, uint256 _amount) internal override {
if (_to == TAX_POOL) {
sumTaxCollected += _amount;
unchecked {
sumTaxCollected += _amount;
}
} else if (ubiTitles[_to].time == 0 && _amount > 0) {
// new account, start UBI title
ubiTitles[_to].sumTaxCollected = sumTaxCollected;
@ -236,8 +277,16 @@ contract Harb is ERC20, ERC20Permit {
/* ============ UBI stuff ============ */
/**
* @notice Calculates the UBI due to an account based on time-weighted average balances.
* @dev Uses historic TWAB data to determine an account's proportionate share of collected taxes since last claim.
* @param _account The account whose UBI is being calculated.
* @param lastTaxClaimed The timestamp of the last UBI claim.
* @param _sumTaxCollected The tax collected up to the last claim.
* @return amountDue The amount of UBI due to the account.
* @return lastPeriodEndAt The timestamp marking the end of the last period considered for UBI calculation.
*/
function ubiDue(address _account, uint256 lastTaxClaimed, uint256 _sumTaxCollected) internal view returns (uint256 amountDue, uint256 lastPeriodEndAt) {
lastPeriodEndAt = ((block.timestamp - PERIOD_OFFSET) / uint256(PERIOD_LENGTH)) * PERIOD_LENGTH + PERIOD_OFFSET - 1;
if (lastTaxClaimed == 0 || lastTaxClaimed > lastPeriodEndAt || lastPeriodEndAt - lastTaxClaimed < PERIOD_LENGTH) {
return (0, lastPeriodEndAt);
@ -248,30 +297,43 @@ contract Harb is ERC20, ERC20Permit {
uint256 taxTwab = twabController.getTwabBetween(address(this), TAX_POOL, lastTaxClaimed, lastPeriodEndAt);
uint256 totalSupplyTwab = twabController.getTotalSupplyTwabBetween(address(this), lastTaxClaimed, lastPeriodEndAt);
uint256 taxCollectedSinceLastClaim = sumTaxCollected - _sumTaxCollected;
//uint256 taxCollectedSinceLastClaim = sumTaxCollected - _sumTaxCollected;
uint256 taxCollectedSinceLastClaim;
if (sumTaxCollected >= _sumTaxCollected) {
taxCollectedSinceLastClaim = sumTaxCollected - _sumTaxCollected;
} else {
// Handle the wrap-around case
taxCollectedSinceLastClaim = type(uint256).max - _sumTaxCollected + sumTaxCollected + 1;
}
amountDue = taxCollectedSinceLastClaim.mulDiv(accountTwab, (totalSupplyTwab - stakeTwab - poolTwab - taxTwab), Math.Rounding.Down);
}
/// @notice Calculates the UBI due to an account based on time-weighted average balances.
/// @dev Uses historic TWAB data to determine an account's proportionate share of collected taxes since last claim.
/// `lastTaxClaimed` is the timestamp of the last UBI claim. `_sumTaxCollected` is the tax collected up to the last claim.
/// @param _account The account whose UBI is being calculated.
/// @return amountDue The amount of UBI due to the account.
/// @return lastPeriodEndAt The timestamp marking the end of the last period considered for UBI calculation.
/**
* @notice Calculates the UBI due to an account based on time-weighted average balances.
* @dev Uses historic TWAB data to determine an account's proportionate share of collected taxes since last claim.
* @param _account The account whose UBI is being calculated.
* @return amountDue The amount of UBI due to the account.
* @return lastPeriodEndAt The timestamp marking the end of the last period considered for UBI calculation.
*/
function getUbiDue(address _account) public view returns (uint256 amountDue, uint256 lastPeriodEndAt) {
UbiTitle storage lastUbiTitle = ubiTitles[_account];
return ubiDue(_account, lastUbiTitle.time, lastUbiTitle.sumTaxCollected);
}
/// @notice Claims the calculated UBI amount for the caller.
/// @dev Transfers the due UBI from the tax pool to the account, updating the UBI title.
/// Emits UbiClaimed event on successful transfer.
/**
* @notice Claims the calculated UBI amount for the caller.
* @dev Transfers the due UBI from the tax pool to the account, updating the UBI title.
* Emits UbiClaimed event on successful transfer.
* @param _account The account claiming the UBI.
* @return ubiAmountDue The amount of UBI claimed.
*/
function claimUbi(address _account) external returns (uint256 ubiAmountDue) {
UbiTitle storage lastUbiTitle = ubiTitles[_account];
uint256 lastPeriodEndAt;
(ubiAmountDue, lastPeriodEndAt) = ubiDue(_account, lastUbiTitle.time, lastUbiTitle.sumTaxCollected);
if (ubiAmountDue > 0) {
ubiTitles[_account].sumTaxCollected = sumTaxCollected;
ubiTitles[_account].time = lastPeriodEndAt;
twabController.transfer(TAX_POOL, _account, SafeCast.toUint96(ubiAmountDue));
@ -280,5 +342,4 @@ contract Harb is ERC20, ERC20Permit {
revert("No UBI to claim.");
}
}
}

View file

@ -12,20 +12,23 @@ import "@openzeppelin/token/ERC20/IERC20.sol";
import "@openzeppelin/utils/math/SignedMath.sol";
import {ABDKMath64x64} from "@abdk/ABDKMath64x64.sol";
import "./interfaces/IWETH9.sol";
import {Harb} from "./Harb.sol";
import {Harberg} from "./Harberg.sol";
/**
* @title LiquidityManager for Harb Token on Uniswap V3
* @notice Manages liquidity provisioning on Uniswap V3 for the Harb token by maintaining three distinct positions:
* - Floor Position: Ensures a minimum price support by having enough reserve assets to potentially buy back the circulating supply of Harb.
* @title LiquidityManager for Harberg Token on Uniswap V3
* @notice Manages liquidity provisioning on Uniswap V3 for the Harberg token by maintaining three distinct positions:
* - Floor Position: Ensures a minimum price support by having enough reserve assets to potentially buy back the circulating supply of Harberg.
* - Anchor Position: Provides liquidity around the current market price to facilitate trading and maintain market stability.
* - Discovery Position: Expands liquidity by minting new Harb tokens as the price rises, capturing potential growth in the ecosystem.
* The contract dynamically adjusts these positions in response to market movements to maintain strategic liquidity levels and support the Harb token's price.
* - Discovery Position: Expands liquidity by minting new Harberg tokens as the price rises, capturing potential growth in the ecosystem.
* The contract dynamically adjusts these positions in response to market movements to maintain strategic liquidity levels and support the Harberg token's price.
* It also collects and transfers fees generated from trading activities to a designated fee destination.
* @dev Utilizes Uniswap V3's concentrated liquidity feature, enabling highly efficient use of capital.
*/
contract LiquidityManager {
// State variables to track total ETH spent
uint256 public cumulativeVolumeWeightedPrice;
uint256 public cumulativeVolume;
// the minimum granularity of liquidity positions in the Uniswap V3 pool. this is a 1% pool.
int24 internal constant TICK_SPACING = 200;
// defines the width of the anchor position from the current price to discovery position.
@ -52,7 +55,7 @@ contract LiquidityManager {
// the address of the Uniswap V3 factory
address private immutable factory;
IWETH9 private immutable weth;
Harb private immutable harb;
Harberg private immutable harb;
IUniswapV3Pool private immutable pool;
bool private immutable token0isWeth;
PoolKey private poolKey;
@ -68,9 +71,6 @@ contract LiquidityManager {
}
mapping(Stage => TokenPosition) public positions;
// State variables to track total ETH spent
uint256 public cumulativeVolumeWeightedPrice;
uint256 public cumulativeVolume;
// the address where liquidity fees will be sent
address public feeDestination;
// the minimum share of ETH that will be put into the anchor
@ -90,17 +90,17 @@ contract LiquidityManager {
_;
}
/// @notice Creates a liquidity manager for managing Harb token liquidity on Uniswap V3.
/// @notice Creates a liquidity manager for managing Harberg token liquidity on Uniswap V3.
/// @param _factory The address of the Uniswap V3 factory.
/// @param _WETH9 The address of the WETH contract for handling ETH in trades.
/// @param _harb The address of the Harb token contract.
/// @dev Computes the Uniswap pool address for the Harb-WETH pair and sets up the initial configuration for the liquidity manager.
/// @param _harb The address of the Harberg token contract.
/// @dev Computes the Uniswap pool address for the Harberg-WETH pair and sets up the initial configuration for the liquidity manager.
constructor(address _factory, address _WETH9, address _harb) {
factory = _factory;
weth = IWETH9(_WETH9);
poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE);
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
harb = Harb(_harb);
harb = Harberg(_harb);
token0isWeth = _WETH9 < _harb;
anchorLiquidityShare = MAX_ANCHOR_LIQ_SHARE;
capitalInfefficiency = MIN_CAPITAL_INEFFICIENCY;
@ -109,7 +109,7 @@ contract LiquidityManager {
/// @notice Callback function that Uniswap V3 calls for liquidity actions requiring minting or burning of tokens.
/// @param amount0Owed The amount of token0 owed for the liquidity provision.
/// @param amount1Owed The amount of token1 owed for the liquidity provision.
/// @dev This function mints Harb tokens as needed and handles WETH deposits for ETH conversions during liquidity interactions.
/// @dev This function mints Harberg tokens as needed and handles WETH deposits for ETH conversions during liquidity interactions.
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
CallbackValidation.verifyCallback(factory, poolKey);
// take care of harb
@ -152,9 +152,9 @@ contract LiquidityManager {
receive() external payable {
}
/// @notice Calculates the Uniswap V3 tick corresponding to a given price ratio between Harb and ETH.
/// @notice Calculates the Uniswap V3 tick corresponding to a given price ratio between Harberg and ETH.
/// @param t0isWeth Boolean flag indicating if token0 is WETH.
/// @param tokenAmount Amount of the Harb token.
/// @param tokenAmount Amount of the Harberg token.
/// @param ethAmount Amount of Ethereum.
/// @return tick_ The calculated tick for the given price ratio.
function tickAtPrice(bool t0isWeth, uint256 tokenAmount, uint256 ethAmount) internal pure returns (int24 tick_) {
@ -363,8 +363,15 @@ contract LiquidityManager {
IERC20(address(weth)).transfer(feeDestination, fee0);
uint256 volume = fee0 * 100;
uint256 volumeWeightedPrice = currentPrice * volume;
cumulativeVolumeWeightedPrice += volumeWeightedPrice;
cumulativeVolume += volume;
// Check for potential overflow
if (cumulativeVolumeWeightedPrice > type(uint256).max - volumeWeightedPrice) {
// Handle overflow: reset
cumulativeVolumeWeightedPrice = volumeWeightedPrice;
cumulativeVolume = volume;
} else {
cumulativeVolumeWeightedPrice += volumeWeightedPrice;
cumulativeVolume += volume;
}
} else {
IERC20(address(harb)).transfer(feeDestination, fee0);
}
@ -376,8 +383,15 @@ contract LiquidityManager {
IERC20(address(weth)).transfer(feeDestination, fee1);
uint256 volume = fee1 * 100;
uint256 volumeWeightedPrice = currentPrice * volume;
cumulativeVolumeWeightedPrice += volumeWeightedPrice;
cumulativeVolume += volume;
// Check for potential overflow
if (cumulativeVolumeWeightedPrice > type(uint256).max - volumeWeightedPrice) {
// Handle overflow: reset
cumulativeVolumeWeightedPrice = volumeWeightedPrice;
cumulativeVolume = volume;
} else {
cumulativeVolumeWeightedPrice += volumeWeightedPrice;
cumulativeVolume += volume;
}
}
}
}
@ -401,7 +415,7 @@ contract LiquidityManager {
return (currentTick >= averageTick - MAX_TICK_DEVIATION && currentTick <= averageTick + MAX_TICK_DEVIATION);
}
/// @notice Adjusts liquidity positions in response to an increase or decrease in the Harb token's price.
/// @notice Adjusts liquidity positions in response to an increase or decrease in the Harberg token's price.
/// @dev This function should be called when significant price movement is detected. It recalibrates the liquidity ranges to align with the new market conditions.
function recenter() external {
// Fetch the current tick from the Uniswap V3 pool

View file

@ -2,18 +2,18 @@
pragma solidity ^0.8.19;
import {IERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
import {IERC20Metadata} from "@openzeppelin/token/ERC20/extensions/IERC20Metadata.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 "./Harb.sol";
import {Harberg} from "./Harberg.sol";
error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake);
error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStake, uint256 smallestShare);
/**
* @title Stake Contract for Harb Token
* @notice This contract manages the staking positions for the Harb token, allowing users to stake tokens
* @title Stake Contract for Harberg Token
* @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.
*
@ -49,11 +49,11 @@ contract Stake {
error NoPermission(address requester, address owner);
error PositionNotFound(uint256 positionId, address requester);
event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 harbDeposit, uint256 share, uint32 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 harbPayout);
event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 harbPayout);
event PositionShrunk(uint256 indexed positionId, address indexed owner, uint256 newShares, uint256 harbergPayout);
event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 harbergPayout);
struct StakingPosition {
uint256 share;
@ -63,7 +63,7 @@ contract Stake {
uint32 taxRate; // e.g. value of 60 = 60% tax per year
}
Harb private immutable harb;
Harberg private immutable harberg;
address private immutable taxPool;
uint256 public immutable totalSupply;
@ -72,13 +72,13 @@ contract Stake {
mapping(uint256 => StakingPosition) public positions;
/// @notice Initializes the stake contract with references to the Harb contract and sets the initial position ID.
/// @param _harb Address of the Harb contract which this Stake contract interacts with.
/// @dev Sets up the total supply based on the decimals of the Harb token plus a fixed offset.
constructor(address _harb) {
harb = Harb(_harb);
totalSupply = 10 ** (harb.decimals() + DECIMAL_OFFSET);
taxPool = Harb(_harb).TAX_POOL();
/// @notice Initializes the stake contract with references to the Harberg contract and sets the initial position ID.
/// @param _harberg Address of the Harberg contract which this Stake contract interacts with.
/// @dev Sets up the total supply based on the decimals of the Harberg token plus a fixed offset.
constructor(address _harberg) {
harberg = Harberg(_harberg);
totalSupply = 10 ** (harberg.decimals() + DECIMAL_OFFSET);
taxPool = Harberg(_harberg).TAX_POOL();
// start counting somewhere
nextPositionId = 654321;
}
@ -100,8 +100,6 @@ contract Stake {
// can not pay more tax than value of position
taxAmountDue = assetsBefore;
}
SafeERC20.safeTransfer(harb, taxPool, taxAmountDue);
if (assetsBefore - taxAmountDue > 0) {
// if something left over, update storage
uint256 shareAfterTax = assetsToShares(assetsBefore - taxAmountDue);
@ -118,9 +116,10 @@ contract Stake {
delete pos.creationTime;
delete pos.share;
}
SafeERC20.safeTransfer(harberg, taxPool, taxAmountDue);
}
/// @dev Internal function to close a staking position, transferring the remaining Harb tokens back to the owner after tax payment.
/// @dev Internal function to close a staking position, transferring the remaining Harberg tokens back to the owner after tax payment.
function _exitPosition(uint256 positionId, StakingPosition storage pos) private {
outstandingStake -= pos.share;
address owner = pos.owner;
@ -129,35 +128,35 @@ contract Stake {
delete pos.owner;
delete pos.creationTime;
delete pos.share;
SafeERC20.safeTransfer(harb, owner, assets);
SafeERC20.safeTransfer(harberg, owner, assets);
}
/// @dev Internal function to reduce the size of a staking position by a specified number of shares, transferring the corresponding Harb tokens to the owner.
/// @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");
uint256 assets = sharesToAssets(sharesToTake);
pos.share -= sharesToTake;
outstandingStake -= sharesToTake;
emit PositionShrunk(positionId, pos.owner, pos.share, assets);
SafeERC20.safeTransfer(harb, pos.owner, assets);
SafeERC20.safeTransfer(harberg, pos.owner, assets);
}
/// @notice Converts Harb token assets to shares of the total staking pool.
/// @param assets Number of Harb tokens to convert.
/// @return Number of shares corresponding to the input assets based on the current total supply of Harb tokens.
/// @notice Converts Harberg token assets to shares of the total staking pool.
/// @param assets Number of Harberg tokens to convert.
/// @return Number of shares corresponding to the input assets based on the current total supply of Harberg tokens.
function assetsToShares(uint256 assets) public view returns (uint256) {
return assets.mulDiv(totalSupply, harb.totalSupply(), Math.Rounding.Down);
return assets.mulDiv(totalSupply, harberg.totalSupply(), Math.Rounding.Down);
}
/// @notice Converts shares of the total staking pool back to Harb token assets.
/// @notice Converts shares of the total staking pool back to Harberg token assets.
/// @param shares Number of shares to convert.
/// @return The equivalent number of Harb tokens for the given shares.
/// @return The equivalent number of Harberg tokens for the given shares.
function sharesToAssets(uint256 shares) public view returns (uint256) {
return shares.mulDiv(harb.totalSupply(), totalSupply, Math.Rounding.Down);
return shares.mulDiv(harberg.totalSupply(), totalSupply, Math.Rounding.Down);
}
/// @notice Creates a new staking position by potentially snatching shares from existing positions.
/// @param assets Amount of Harb tokens to convert into a staking position.
/// @param assets Amount of Harberg tokens to convert into a staking position.
/// @param receiver Address that will own the new staking position.
/// @param taxRate The initial tax rate for the new staking position.
/// @param positionsToSnatch Array of position IDs that the new position will replace by snatching.
@ -172,7 +171,7 @@ contract Stake {
{
// check that position size is at least minStake
// to prevent excessive fragmentation, increasing snatch cost
uint256 minStake = harb.minStake();
uint256 minStake = harberg.minStake();
if (assets < minStake) {
revert StakeTooLow(receiver, assets, minStake);
}
@ -241,7 +240,7 @@ contract Stake {
}
// transfer
SafeERC20.safeTransferFrom(harb, msg.sender, address(this), assets);
SafeERC20.safeTransferFrom(harberg, msg.sender, address(this), assets);
// mint
positionId = nextPositionId++;
@ -257,7 +256,7 @@ contract Stake {
}
/// @notice Combines an ERC20 permit operation with the snatch function, allowing a staking position creation in one transaction.
/// @param assets Number of Harb tokens to stake.
/// @param assets Number of Harberg tokens to stake.
/// @param receiver Address that will own the new staking position.
/// @param taxRate The initial tax rate for the new staking position.
/// @param positionsToSnatch Array of position IDs that the new position will replace by snatching.
@ -279,7 +278,7 @@ contract Stake {
) external
returns (uint256 positionId)
{
ERC20Permit(address(harb)).permit(receiver, address(this), assets, deadline, v, r, s);
ERC20Permit(address(harberg)).permit(receiver, address(this), assets, deadline, v, r, s);
return snatch(assets, receiver, taxRate, positionsToSnatch);
}

View file

@ -4,11 +4,11 @@ pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
import "../src/Harb.sol";
import "../src/Harberg.sol";
contract HarbTest is Test {
contract HarbergTest is Test {
TwabController tc;
Harb harb;
Harberg harberg;
address stakingPool;
address liquidityPool;
address liquidityManager;
@ -16,37 +16,37 @@ contract HarbTest is Test {
function setUp() public {
tc = new TwabController(60 * 60, uint32(block.timestamp));
harb = new Harb("HARB", "HARB", tc);
taxPool = harb.TAX_POOL();
harberg = new Harberg("HARB", "HARB", tc);
taxPool = harberg.TAX_POOL();
stakingPool = makeAddr("stakingPool");
harb.setStakingPool(stakingPool);
harberg.setStakingPool(stakingPool);
liquidityPool = makeAddr("liquidityPool");
harb.setLiquidityPool(liquidityPool);
harberg.setLiquidityPool(liquidityPool);
liquidityManager = makeAddr("liquidityManager");
harb.setLiquidityManager(liquidityManager);
harberg.setLiquidityManager(liquidityManager);
}
// Simulates staking by transferring tokens to the stakingPool address.
function simulateStake(uint256 amount) internal {
// the amount of token has to be available on the balance
// of the test contract
harb.transfer(stakingPool, amount);
harberg.transfer(stakingPool, amount);
}
// 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
harb.transfer(address(this), amount);
harberg.transfer(address(this), amount);
}
function testHarbConstructor() public view {
function testHarbergConstructor() public view {
// Check if the token details are set as expected
assertEq(harb.name(), "HARB");
assertEq(harb.symbol(), "HARB");
assertEq(harberg.name(), "HARB");
assertEq(harberg.symbol(), "HARB");
// Confirm that the TwabController address is correctly set
(address _tc, address _lm, address _sp, address _lp) = harb.getPeripheryContracts();
(address _tc, address _lm, address _sp, address _lp) = harberg.peripheryContracts();
assertEq(_tc, address(tc));
assertEq(_lm, liquidityManager);
assertEq(_sp, stakingPool);
@ -54,67 +54,67 @@ contract HarbTest is Test {
}
function testMintWithEmptyStakingPool() public {
uint256 initialSupply = harb.totalSupply();
uint256 initialSupply = harberg.totalSupply();
uint256 mintAmount = 1000 * 1e18; // 1000 HARB tokens
vm.prank(address(liquidityManager));
harb.mint(mintAmount);
harberg.mint(mintAmount);
// Check if the total supply has increased correctly
assertEq(harb.totalSupply(), initialSupply + mintAmount);
assertEq(harberg.totalSupply(), initialSupply + mintAmount);
// Check if the staking pool balance is still 0, as before
assertEq(harb.balanceOf(stakingPool), 0);
assertEq(harberg.balanceOf(stakingPool), 0);
}
function testBurnWithEmptyStakingPool() public {
uint256 initialSupply = harb.totalSupply();
uint256 initialSupply = harberg.totalSupply();
uint256 burnAmount = 500 * 1e18; // 500 HARB tokens
// First, mint some tokens to burn
vm.prank(address(liquidityManager));
harb.mint(burnAmount);
harberg.mint(burnAmount);
vm.prank(address(liquidityManager));
harb.burn(burnAmount);
harberg.burn(burnAmount);
// Check if the total supply has decreased correctly
assertEq(harb.totalSupply(), initialSupply);
assertEq(harberg.totalSupply(), initialSupply);
// Check if the staking pool balance has decreased correctly
assertEq(harb.balanceOf(stakingPool), 0);
assertEq(harberg.balanceOf(stakingPool), 0);
}
function testMintImpactOnSimulatedStaking() public {
uint256 initialStakingPoolBalance = harb.balanceOf(stakingPool);
uint256 initialStakingPoolBalance = harberg.balanceOf(stakingPool);
uint256 mintAmount = 1000 * 1e18; // 1000 HARB tokens
// Ensure the test contract has enough tokens to simulate staking
vm.prank(address(liquidityManager));
harb.mint(mintAmount);
harberg.mint(mintAmount);
vm.prank(address(liquidityManager));
harb.transfer(address(this), mintAmount);
harberg.transfer(address(this), mintAmount);
// Simulate staking of the minted amount
simulateStake(mintAmount);
// Check balances after simulated staking
assertEq(harb.balanceOf(stakingPool), initialStakingPoolBalance + mintAmount);
assertEq(harberg.balanceOf(stakingPool), initialStakingPoolBalance + mintAmount);
}
function testUnstakeImpactOnTotalSupply() public {
uint256 stakeAmount = 500 * 1e18; // 500 HARB tokens
// Ensure the test contract has enough tokens to simulate staking
vm.prank(address(liquidityManager));
harb.mint(stakeAmount);
harberg.mint(stakeAmount);
vm.prank(address(liquidityManager));
harb.transfer(address(this), stakeAmount);
harberg.transfer(address(this), stakeAmount);
uint256 initialTotalSupply = harb.totalSupply();
uint256 initialTotalSupply = harberg.totalSupply();
// Simulate staking and then unstaking
simulateStake(stakeAmount);
simulateUnstake(stakeAmount);
// Check total supply remains unchanged after unstake
assertEq(harb.totalSupply(), initialTotalSupply);
assertEq(harberg.totalSupply(), initialTotalSupply);
}
// Fuzz test for mint function with varying stake amounts
@ -122,29 +122,29 @@ contract HarbTest is Test {
uint256 initialAmount = 500 * 1e18;
// Ensure the test contract has enough tokens to simulate staking
vm.prank(address(liquidityManager));
harb.mint(initialAmount);
harberg.mint(initialAmount);
vm.prank(address(liquidityManager));
harb.transfer(address(this), initialAmount);
harberg.transfer(address(this), initialAmount);
// Limit fuzzing input to 0% - 20%
uint8 effectiveStakePercentage = _stakePercentage % 21;
uint256 stakeAmount = (initialAmount * effectiveStakePercentage) / 100;
simulateStake(stakeAmount);
uint256 initialTotalSupply = harb.totalSupply();
uint256 initialStakingPoolBalance = harb.balanceOf(stakingPool);
uint256 initialTotalSupply = harberg.totalSupply();
uint256 initialStakingPoolBalance = harberg.balanceOf(stakingPool);
mintAmount = bound(mintAmount, 0, 500 * 1e18);
uint256 expectedNewStake = initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance);
vm.prank(address(liquidityManager));
harb.mint(mintAmount);
harberg.mint(mintAmount);
uint256 expectedStakingPoolBalance = initialStakingPoolBalance + expectedNewStake;
uint256 expectedTotalSupply = initialTotalSupply + mintAmount + expectedNewStake;
assertEq(harb.totalSupply(), expectedTotalSupply, "Total supply did not match expected after mint.");
assertEq(harb.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after mint.");
assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply did not match expected after mint.");
assertEq(harberg.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after mint.");
}
// Fuzz test for burn function with varying stake amounts
@ -152,45 +152,45 @@ contract HarbTest is Test {
uint256 mintAmount = 500 * 1e18;
// Ensure the test contract has enough tokens to simulate staking
vm.prank(address(liquidityManager));
harb.mint(mintAmount);
harberg.mint(mintAmount);
// Limit fuzzing input to 0% - 20%
uint8 effectiveStakePercentage = _stakePercentage % 21;
uint256 stakeAmount = (mintAmount * effectiveStakePercentage) / 100;
vm.prank(address(liquidityManager));
harb.transfer(address(this), stakeAmount);
harberg.transfer(address(this), stakeAmount);
simulateStake(stakeAmount);
burnAmount = bound(burnAmount, 0, 200 * 1e18);
uint256 initialTotalSupply = harb.totalSupply();
uint256 initialStakingPoolBalance = harb.balanceOf(stakingPool);
uint256 initialTotalSupply = harberg.totalSupply();
uint256 initialStakingPoolBalance = harberg.balanceOf(stakingPool);
uint256 expectedExcessStake = initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance);
vm.prank(address(liquidityManager));
harb.burn(burnAmount);
harberg.burn(burnAmount);
uint256 expectedStakingPoolBalance = initialStakingPoolBalance - expectedExcessStake;
uint256 expectedTotalSupply = initialTotalSupply - burnAmount - expectedExcessStake;
assertEq(harb.totalSupply(), expectedTotalSupply, "Total supply did not match expected after burn.");
assertEq(harb.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after burn.");
assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply did not match expected after burn.");
assertEq(harberg.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after burn.");
}
function testTaxAccumulation() public {
uint256 taxAmount = 100 * 1e18; // 100 HARB tokens
vm.prank(address(liquidityManager));
harb.mint(taxAmount);
harberg.mint(taxAmount);
// Initial tax collected should be zero
assertEq(harb.sumTaxCollected(), 0, "Initial tax collected should be zero.");
assertEq(harberg.sumTaxCollected(), 0, "Initial tax collected should be zero.");
// Simulate sending tokens to the taxPool
vm.prank(address(liquidityManager));
harb.transfer(taxPool, taxAmount);
harberg.transfer(taxPool, taxAmount);
// Check that sumTaxCollected has been updated correctly
assertEq(harb.sumTaxCollected(), taxAmount, "Tax collected not updated correctly after transfer to taxPool.");
assertEq(harberg.sumTaxCollected(), taxAmount, "Tax collected not updated correctly after transfer to taxPool.");
}
function testUBIClaimBySingleAccountOverTime() public {
@ -200,36 +200,86 @@ contract HarbTest is Test {
// Setup initial supply and distribute to user
vm.prank(address(liquidityManager));
harb.mint(initialSupply + taxAmount);
harberg.mint(initialSupply + taxAmount);
vm.prank(address(liquidityManager));
harb.transfer(user, initialSupply);
harberg.transfer(user, initialSupply);
// Simulate tax collection
vm.prank(address(liquidityManager));
harb.transfer(taxPool, taxAmount);
harberg.transfer(taxPool, taxAmount);
// Simulate time passage to ensure TWAB is recorded over time
vm.warp(block.timestamp + 30 days);
// Assert initial user balance and sumTaxCollected before claiming UBI
assertEq(harb.balanceOf(user), initialSupply, "User should hold the entire initial supply.");
assertEq(harb.sumTaxCollected(), taxAmount, "Tax collected should match the tax amount transferred.");
assertEq(harberg.balanceOf(user), initialSupply, "User should hold the entire initial supply.");
assertEq(harberg.sumTaxCollected(), taxAmount, "Tax collected should match the tax amount transferred.");
// User claims UBI
vm.prank(user);
harb.claimUbi(user);
harberg.claimUbi(user);
// Compute expected UBI
// Assume the user is the only one holding tokens, they get all the collected taxes.
uint256 expectedUbiAmount = taxAmount;
// Verify UBI claim
uint256 postClaimBalance = harb.balanceOf(user);
uint256 postClaimBalance = harberg.balanceOf(user);
assertEq(postClaimBalance, initialSupply + expectedUbiAmount, "User's balance after claiming UBI is incorrect.");
// Ensure that claiming doesn't affect the total supply
uint256 expectedTotalSupply = initialSupply + taxAmount; // Include minted tax
assertEq(harb.totalSupply(), expectedTotalSupply, "Total supply should be unchanged after UBI claim.");
assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply should be unchanged after UBI claim.");
}
function testUBIClaimBySingleAccountWithWraparound() public {
uint256 initialSupply = 1000 * 1e18; // 1000 HARB tokens
uint256 taxAmount = 200 * 1e18; // 200 HARB tokens to be collected as tax
uint256 nearMaxUint = type(uint256).max - 10;
address user = makeAddr("alice");
// Set sumTaxCollected to near max value to simulate wrap-around
vm.store(
address(harberg),
bytes32(uint256(9)),
bytes32(nearMaxUint)
);
// Read the value back to confirm it's set correctly
assertEq(harberg.sumTaxCollected(), nearMaxUint, "Initial sumTaxCollected should be near max uint256");
// Setup initial supply and distribute to user
vm.prank(address(liquidityManager));
harberg.mint(initialSupply + taxAmount);
vm.prank(address(liquidityManager));
harberg.transfer(user, initialSupply);
// Simulate tax collection to cause overflow
vm.prank(address(liquidityManager));
harberg.transfer(taxPool, taxAmount);
// Simulate time passage to ensure TWAB is recorded over time
vm.warp(block.timestamp + 30 days);
// Verify the new value of sumTaxCollected after overflow
uint256 newSumTaxCollected = harberg.sumTaxCollected();
assertGt(taxAmount, newSumTaxCollected, "sumTaxCollected should have wrapped around and be less than taxAmount");
// User claims UBI
vm.prank(user);
harberg.claimUbi(user);
// Compute expected UBI
// Assume the user is the only one holding tokens, they get all the collected taxes.
uint256 expectedUbiAmount = taxAmount;
// Verify UBI claim
uint256 postClaimBalance = harberg.balanceOf(user);
assertApproxEqRel(postClaimBalance, initialSupply + expectedUbiAmount, 1 * 1e14, "User's balance after claiming UBI is incorrect.");
// Ensure that claiming doesn't affect the total supply
uint256 expectedTotalSupply = initialSupply + taxAmount; // Include minted tax
assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply should be unchanged after UBI claim.");
}
function testUBIClaimByMultipleAccountsWithDifferentHoldingPeriods() public {
@ -241,10 +291,10 @@ contract HarbTest is Test {
// Setup initial supply and distribute to users
vm.startPrank(address(liquidityManager));
harb.mint(initialSupply);
harb.transfer(account1, 400 * 1e18); // Account 1 gets 400 tokens
harb.transfer(account2, 300 * 1e18); // Account 2 gets 300 tokens
harb.transfer(account3, 300 * 1e18); // Account 3 gets 300 tokens
harberg.mint(initialSupply);
harberg.transfer(account1, 400 * 1e18); // Account 1 gets 400 tokens
harberg.transfer(account2, 300 * 1e18); // Account 2 gets 300 tokens
harberg.transfer(account3, 300 * 1e18); // Account 3 gets 300 tokens
vm.stopPrank();
uint256 startTime = block.timestamp;
@ -252,33 +302,33 @@ contract HarbTest is Test {
// Simulate different holding periods
vm.warp(block.timestamp + 10 days); // Fast forward 10 days
vm.prank(account1);
harb.transfer(account2, 100 * 1e18); // Account 1 transfers 100 tokens to Account 2
harberg.transfer(account2, 100 * 1e18); // Account 1 transfers 100 tokens to Account 2
vm.warp(block.timestamp + 20 days); // Fast forward another 20 days
vm.prank(account2);
harb.transfer(account3, 100 * 1e18); // Account 2 transfers 100 tokens to Account 3
harberg.transfer(account3, 100 * 1e18); // Account 2 transfers 100 tokens to Account 3
// Simulate tax collection after the transactions
vm.startPrank(address(liquidityManager));
harb.mint(taxAmount);
harb.transfer(taxPool, taxAmount);
harberg.mint(taxAmount);
harberg.transfer(taxPool, taxAmount);
vm.stopPrank();
// Assert sumTaxCollected before claiming UBI
assertEq(harb.sumTaxCollected(), taxAmount, "Tax collected should match the tax amount transferred.");
assertEq(harberg.sumTaxCollected(), taxAmount, "Tax collected should match the tax amount transferred.");
// Each account claims UBI
vm.prank(account1);
harb.claimUbi(account1);
harberg.claimUbi(account1);
vm.prank(account2);
harb.claimUbi(account2);
harberg.claimUbi(account2);
vm.prank(account3);
harb.claimUbi(account3);
harberg.claimUbi(account3);
// Assert the post-claim balances reflect the TWAB calculations
{
uint256 totalDistributed = harb.balanceOf(account1) + harb.balanceOf(account2) + harb.balanceOf(account3) - initialSupply;
uint256 totalDistributed = harberg.balanceOf(account1) + harberg.balanceOf(account2) + harberg.balanceOf(account3) - initialSupply;
// Tolerance setup: 0.01% of the total tax amount
uint256 lowerBound = taxAmount - (taxAmount / 10000);
assertTrue(totalDistributed >= lowerBound && totalDistributed <= totalDistributed, "Total distributed UBI does not match the total tax collected within an acceptable tolerance range.");
@ -300,9 +350,9 @@ contract HarbTest is Test {
// Assert the post-claim balances reflect the TWAB calculations with a smaller rounding tolerance
// 1 * 1e14; // 0.0001 HARB token tolerance for rounding errors
assertApproxEqRel(harb.balanceOf(account1) - 300 * 1e18, expectedBalance1, 1 * 1e14, "Account 1's balance after claiming UBI is incorrect.");
assertApproxEqRel(harb.balanceOf(account2) - 300 * 1e18, expectedBalance2, 1 * 1e14, "Account 2's balance after claiming UBI is incorrect.");
assertApproxEqRel(harb.balanceOf(account3) - 400 * 1e18, expectedBalance3, 1 * 1e14, "Account 3's balance after claiming UBI is incorrect.");
assertApproxEqRel(harberg.balanceOf(account1) - 300 * 1e18, expectedBalance1, 1 * 1e14, "Account 1's balance after claiming UBI is incorrect.");
assertApproxEqRel(harberg.balanceOf(account2) - 300 * 1e18, expectedBalance2, 1 * 1e14, "Account 2's balance after claiming UBI is incorrect.");
assertApproxEqRel(harberg.balanceOf(account3) - 400 * 1e18, expectedBalance3, 1 * 1e14, "Account 3's balance after claiming UBI is incorrect.");
}
function testUBIClaimWithoutAnyTaxCollected() public {
@ -311,12 +361,12 @@ contract HarbTest is Test {
// Setup initial supply and allocate to user
vm.startPrank(address(liquidityManager));
harb.mint(initialSupply);
harb.transfer(user, initialSupply);
harberg.mint(initialSupply);
harberg.transfer(user, initialSupply);
vm.stopPrank();
// Ensure no tax has been collected yet
assertEq(harb.sumTaxCollected(), 0, "Initial tax collected should be zero.");
assertEq(harberg.sumTaxCollected(), 0, "Initial tax collected should be zero.");
// Simulate time passage to ensure TWAB is recorded
vm.warp(block.timestamp + 30 days);
@ -324,13 +374,13 @@ contract HarbTest is Test {
// User attempts to claim UBI
vm.prank(user);
vm.expectRevert("No UBI to claim."); // Assuming your contract reverts with a message when there's no UBI to claim
harb.claimUbi(user);
harberg.claimUbi(user);
// Ensure the user's balance remains unchanged as no UBI should be distributed
assertEq(harb.balanceOf(user), initialSupply, "User's balance should not change after attempting to claim UBI without any taxes collected.");
assertEq(harberg.balanceOf(user), initialSupply, "User's balance should not change after attempting to claim UBI without any taxes collected.");
// Check if sumTaxCollected remains zero after the claim attempt
assertEq(harb.sumTaxCollected(), 0, "No tax should be collected, and sumTaxCollected should remain zero after the claim attempt.");
assertEq(harberg.sumTaxCollected(), 0, "No tax should be collected, and sumTaxCollected should remain zero after the claim attempt.");
}
function testEdgeCaseWithMaximumTaxCollection() public {
@ -341,27 +391,27 @@ contract HarbTest is Test {
// Setup initial supply and allocate to user
vm.startPrank(address(liquidityManager));
harb.mint(initialSupply + maxTaxAmount);
harb.transfer(account1, initialSupply);
harb.transfer(taxPool, maxTaxAmount); // Simulate tax collection at the theoretical maximum
harberg.mint(initialSupply + maxTaxAmount);
harberg.transfer(account1, initialSupply);
harberg.transfer(taxPool, maxTaxAmount); // Simulate tax collection at the theoretical maximum
vm.stopPrank();
// Assert that maximum tax was collected
assertEq(harb.sumTaxCollected(), maxTaxAmount, "Max tax collected should match the max tax amount transferred.");
assertEq(harberg.sumTaxCollected(), maxTaxAmount, "Max tax collected should match the max tax amount transferred.");
// Simulate time passage and UBI claim
vm.warp(block.timestamp + 30 days);
// Account 1 claims UBI
vm.prank(account1);
harb.claimUbi(account1);
harberg.claimUbi(account1);
// Check if the account's balance increased correctly
uint256 expectedBalance = initialSupply + maxTaxAmount; // This assumes the entire tax pool goes to one account, simplify as needed
assertEq(harb.balanceOf(account1), expectedBalance, "Account 1's balance after claiming UBI with max tax collection is incorrect.");
assertEq(harberg.balanceOf(account1), expectedBalance, "Account 1's balance after claiming UBI with max tax collection is incorrect.");
// Verify that no taxes are left unclaimed
assertEq(harb.balanceOf(taxPool), 0, "All taxes should be claimed after the UBI claim.");
assertEq(harberg.balanceOf(taxPool), 0, "All taxes should be claimed after the UBI claim.");
}
@ -374,10 +424,10 @@ contract HarbTest is Test {
// Setup initial supply and allocate to user
vm.startPrank(address(liquidityManager));
harb.mint(initialSupply + taxAmount);
harb.transfer(account1, initialSupply / 800);
harb.transfer(taxPool, taxAmount); // Simulate tax collection at the theoretical maximum
harb.transfer(liquidityPool, harb.balanceOf(address(liquidityManager)));
harberg.mint(initialSupply + taxAmount);
harberg.transfer(account1, initialSupply / 800);
harberg.transfer(taxPool, taxAmount); // Simulate tax collection at the theoretical maximum
harberg.transfer(liquidityPool, harberg.balanceOf(address(liquidityManager)));
vm.stopPrank();
vm.warp(block.timestamp + 1 hours);
@ -386,20 +436,20 @@ contract HarbTest is Test {
uint numHours = 399; // More than 365 to potentially test buffer wrapping (MAX_CARDINALITY)
for (uint i = 0; i < numHours; i++) {
vm.prank(liquidityPool);
harb.transfer(account1, initialSupply / 800);
harberg.transfer(account1, initialSupply / 800);
vm.warp(block.timestamp + 1 hours); // Fast-forward time by one hour.
}
// Account 1 claims UBI
vm.prank(account1);
uint256 ubiCollected = harb.claimUbi(account1);
uint256 ubiCollected = harberg.claimUbi(account1);
// Check if the account's balance increased correctly
uint256 expectedBalance = (initialSupply / 2) + ubiCollected; // This assumes the entire tax pool goes to one account, simplify as needed
assertApproxEqRel(harb.balanceOf(account1), expectedBalance, 1 * 1e18, "Account 1's balance after claiming UBI with max tax collection is incorrect.");
assertApproxEqRel(harberg.balanceOf(account1), expectedBalance, 1 * 1e18, "Account 1's balance after claiming UBI with max tax collection is incorrect.");
// Verify that no taxes are left unclaimed
assertEq(harb.balanceOf(taxPool), 0, "All taxes should be claimed after the UBI claim.");
assertEq(harberg.balanceOf(taxPool), 0, "All taxes should be claimed after the UBI claim.");
}
}

View file

@ -10,7 +10,7 @@ import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
import {PoolAddress, PoolKey} from "@aperture/uni-v3-lib/PoolAddress.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import {Harb} from "../src/Harb.sol";
import {Harberg} from "../src/Harberg.sol";
import {Stake, ExceededAvailableStake} from "../src/Stake.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
@ -30,7 +30,7 @@ contract Dummy {
contract LiquidityManagerTest is Test {
IWETH9 weth;
Harb harb;
Harberg harberg;
IUniswapV3Factory factory;
Stake stake;
LiquidityManager lm;
@ -103,30 +103,30 @@ contract LiquidityManagerTest is Test {
}
weth = IWETH9(address(new WETH()));
harb = new Harb("HARB", "HARB", tc);
harberg = new Harberg("HARB", "HARB", tc);
// Check if the setup meets the required condition
if (token0shouldBeWeth == address(weth) < address(harb)) {
if (token0shouldBeWeth == address(weth) < address(harberg)) {
setupComplete = true;
} else {
// Clear current instances for re-deployment
delete weth;
delete harb;
delete harberg;
retryCount++;
}
}
require(setupComplete, "Setup failed to meet the condition after several retries");
pool = IUniswapV3Pool(factory.createPool(address(weth), address(harb), FEE));
pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE));
token0isWeth = address(weth) < address(harb);
token0isWeth = address(weth) < address(harberg);
initializePoolFor1Cent(address(pool));
stake = new Stake(address(harb));
harb.setStakingPool(address(stake));
lm = new LiquidityManager(factoryAddress, address(weth), address(harb));
stake = new Stake(address(harberg));
harberg.setStakingPool(address(stake));
lm = new LiquidityManager(factoryAddress, address(weth), address(harberg));
lm.setFeeDestination(feeDestination);
harb.setLiquidityManager(address(lm));
harberg.setLiquidityManager(address(lm));
vm.deal(address(lm), 10 ether);
createCSVHeader();
}
@ -139,10 +139,10 @@ contract LiquidityManagerTest is Test {
try lm.recenter() {
// Check liquidity positions after slide
(uint256 ethFloor, uint256 ethAnchor, uint256 ethDiscovery, uint256 harbFloor, uint256 harbAnchor, uint256 harbDiscovery) = checkLiquidityPositionsAfter("slide");
(uint256 ethFloor, uint256 ethAnchor, uint256 ethDiscovery, uint256 harbergFloor, uint256 harbergAnchor, uint256 harbergDiscovery) = checkLiquidityPositionsAfter("slide");
assertGt(ethFloor, ethAnchor, "slide - Floor should hold more ETH than Anchor");
assertGt(harbDiscovery, harbAnchor * 5, "slide - Discovery should hold more HARB than Anchor");
assertEq(harbFloor, 0, "slide - Floor should have no HARB");
assertGt(harbergDiscovery, harbergAnchor * 5, "slide - Discovery should hold more HARB than Anchor");
assertEq(harbergFloor, 0, "slide - Floor should have no HARB");
assertEq(ethDiscovery, 0, "slide - Discovery should have no ETH");
} catch Error(string memory reason) {
if (keccak256(abi.encodePacked(reason)) == keccak256(abi.encodePacked("amplitude not reached."))) {
@ -162,10 +162,10 @@ contract LiquidityManagerTest is Test {
try lm.recenter() {
// Check liquidity positions after shift
(uint256 ethFloor, uint256 ethAnchor, uint256 ethDiscovery, uint256 harbFloor, uint256 harbAnchor, uint256 harbDiscovery) = checkLiquidityPositionsAfter("shift");
(uint256 ethFloor, uint256 ethAnchor, uint256 ethDiscovery, uint256 harbergFloor, uint256 harbergAnchor, uint256 harbergDiscovery) = checkLiquidityPositionsAfter("shift");
assertGt(ethFloor, ethAnchor, "shift - Floor should hold more ETH than Anchor");
assertGt(harbDiscovery, harbAnchor * 5, "shift - Discovery should hold more HARB than Anchor");
assertEq(harbFloor, 0, "shift - Floor should have no HARB");
assertGt(harbergDiscovery, harbergAnchor * 5, "shift - Discovery should hold more HARB than Anchor");
assertEq(harbergFloor, 0, "shift - Floor should have no HARB");
assertEq(ethDiscovery, 0, "shift - Discovery should have no ETH");
} catch Error(string memory reason) {
if (keccak256(abi.encodePacked(reason)) == keccak256(abi.encodePacked("amplitude not reached."))) {
@ -177,7 +177,7 @@ contract LiquidityManagerTest is Test {
}
function getBalancesPool(LiquidityManager.Stage s) internal view returns (int24 currentTick, int24 tickLower, int24 tickUpper, uint256 ethAmount, uint256 harbAmount) {
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)));
@ -192,46 +192,46 @@ contract LiquidityManagerTest is Test {
if (currentTick < tickLower) {
// Current price is below the lower bound of the liquidity position
ethAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
harbAmount = 0; // All liquidity is in token0 (ETH)
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)
harbAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
harbergAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
} else {
// Current price is within the bounds of the liquidity position
ethAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
harbAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
harbergAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
}
} else {
if (currentTick < tickLower) {
// Current price is below the lower bound of the liquidity position
harbAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
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
harbAmount = 0; // All liquidity is in token0 (HARB)
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
harbAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
harbergAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
ethAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
}
}
}
// csv = "precedingAction, currentTick, floorTickLower, floorTickUpper, floorEth, floorHarb, anchorTickLower, anchorTickUpper, anchorEth, anchorHarb, discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryHarb";
function checkLiquidityPositionsAfter(string memory eventName) internal returns (uint ethFloor, uint ethAnchor, uint ethDiscovery, uint harbFloor, uint harbAnchor, uint harbDiscovery) {
function checkLiquidityPositionsAfter(string memory eventName) internal returns (uint ethFloor, uint ethAnchor, uint ethDiscovery, uint harbergFloor, uint harbergAnchor, uint harbergDiscovery) {
int24 currentTick;
int24 tickLower;
int24 tickUpper;
(currentTick, tickLower, tickUpper, ethFloor, harbFloor) = getBalancesPool(LiquidityManager.Stage.FLOOR);
string memory floorData = string(abi.encodePacked(intToStr(tickLower), ",", intToStr(tickUpper), ",", uintToStr(ethFloor), ",", uintToStr(harbFloor), ","));
(currentTick, tickLower, tickUpper, ethFloor, harbergFloor) = getBalancesPool(LiquidityManager.Stage.FLOOR);
string memory floorData = string(abi.encodePacked(intToStr(tickLower), ",", intToStr(tickUpper), ",", uintToStr(ethFloor), ",", uintToStr(harbergFloor), ","));
(,tickLower, tickUpper, ethAnchor, harbAnchor) = getBalancesPool(LiquidityManager.Stage.ANCHOR);
string memory anchorData = string(abi.encodePacked(intToStr(tickLower), ",", intToStr(tickUpper), ",", uintToStr(ethAnchor), ",", uintToStr(harbAnchor), ","));
(,tickLower, tickUpper, ethAnchor, harbergAnchor) = getBalancesPool(LiquidityManager.Stage.ANCHOR);
string memory anchorData = string(abi.encodePacked(intToStr(tickLower), ",", intToStr(tickUpper), ",", uintToStr(ethAnchor), ",", uintToStr(harbergAnchor), ","));
(,tickLower, tickUpper, ethDiscovery, harbDiscovery) = getBalancesPool(LiquidityManager.Stage.DISCOVERY);
string memory discoveryData = string(abi.encodePacked(intToStr(tickLower), ",", intToStr(tickUpper), ",", uintToStr(ethDiscovery), ",", uintToStr(harbDiscovery), ","));
(,tickLower, tickUpper, ethDiscovery, harbergDiscovery) = getBalancesPool(LiquidityManager.Stage.DISCOVERY);
string memory discoveryData = string(abi.encodePacked(intToStr(tickLower), ",", intToStr(tickUpper), ",", uintToStr(ethDiscovery), ",", uintToStr(harbergDiscovery), ","));
csv = string(abi.encodePacked(csv, "\n", eventName, ",", intToStr(currentTick), ",", floorData, anchorData, discoveryData));
}
@ -262,11 +262,11 @@ contract LiquidityManagerTest is Test {
limit = token0isWeth ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1;
} else {
// vm.startPrank(address(lm));
// harb.mint(amount);
// harb.transfer(address(account), amount);
// harberg.mint(amount);
// harberg.transfer(address(account), amount);
// vm.stopPrank();
vm.prank(account);
harb.approve(address(this), amount);
harberg.approve(address(this), amount);
limit = !token0isWeth ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1;
}
pool.swap(
@ -300,7 +300,7 @@ contract LiquidityManagerTest is Test {
return;
}
require(harb.transferFrom(seller, msg.sender, amountToPay), "reason 3");
require(harberg.transferFrom(seller, msg.sender, amountToPay), "reason 3");
}
receive() external payable {}
@ -368,6 +368,48 @@ contract LiquidityManagerTest is Test {
vm.writeFile(path, csv);
}
function testHandleCumulativeOverflow() public {
setUpCustomToken0(false);
vm.deal(account, 201 ether);
vm.prank(account);
weth.deposit{value: 201 ether}();
// Setup initial liquidity
slide(false);
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 cumulativeVolumeWeightedPrice = lm.cumulativeVolumeWeightedPrice();
uint256 beforeCumulativeVolume = lm.cumulativeVolume();
assertGt(cumulativeVolumeWeightedPrice, type(uint256).max / 2, "Initial cumulativeVolumeWeightedPrice is not near max uint256");
buy(50 ether);
shift();
cumulativeVolumeWeightedPrice = lm.cumulativeVolumeWeightedPrice();
uint256 cumulativeVolume = lm.cumulativeVolume();
// Assert that the values after wrap-around are valid and smaller than max uint256
assertGt(beforeCumulativeVolume, cumulativeVolume, "cumulativeVolume after wrap-around is smaller than before");
// Assert that the price is reasonable
uint256 calculatedPrice = cumulativeVolumeWeightedPrice / cumulativeVolume;
assertTrue(calculatedPrice > 0 && calculatedPrice < 10**40, "Calculated price after wrap-around is not within a reasonable range");
}
// function testLiquidityPositions() public {
// setUpCustomToken0(false);
@ -375,15 +417,15 @@ contract LiquidityManagerTest is Test {
// slide();
// // Initial checks of liquidity positions
// (uint ethFloor, uint ethAnchor, uint ethDiscovery, uint harbFloor, uint harbAnchor, uint harbDiscovery) = checkLiquidityPositionsAfter("slide");
// (uint ethFloor, uint ethAnchor, uint ethDiscovery, uint harbergFloor, uint harbergAnchor, uint harbergDiscovery) = checkLiquidityPositionsAfter("slide");
// // Assertions to verify initial setup
// assertGt(ethFloor, 9 ether, "Floor should have initial ETH");
// assertGt(ethAnchor, 0.8 ether, "Anchor should have initial ETH");
// assertEq(ethDiscovery, 0, "Discovery should not have ETH");
// assertEq(harbFloor, 0, "Floor should have no HARB");
// assertGt(harbAnchor, 0, "Anchor should have HARB");
// assertGt(harbDiscovery, 0, "Discovery should have HARB");
// assertEq(harbergFloor, 0, "Floor should have no HARB");
// assertGt(harbergAnchor, 0, "Anchor should have HARB");
// assertGt(harbergDiscovery, 0, "Discovery should have HARB");
// // Introduce large buy to push into discovery
// buy(3 ether);
@ -396,30 +438,30 @@ contract LiquidityManagerTest is Test {
// shift();
// // Check liquidity positions after shift
// (ethFloor, ethAnchor, ethDiscovery, harbFloor, , ) = checkLiquidityPositionsAfter("shift");
// (ethFloor, ethAnchor, ethDiscovery, harbergFloor, , ) = checkLiquidityPositionsAfter("shift");
// assertGt(ethFloor, 11.5 ether, "Floor should have more ETH");
// assertGt(ethAnchor, 1.4 ether, "Anchor should have more ETH");
// assertEq(ethDiscovery, 0, "Discovery should not have ETH after shift");
// assertEq(harbFloor, 0, "Floor should have no HARB");
// assertEq(harbergFloor, 0, "Floor should have no HARB");
// // Simulate large sell to push price down to floor
// sell(6 * 10**23);
// // Check liquidity positions after sell
// (ethFloor, ethAnchor, ethDiscovery, harbFloor, harbAnchor, harbDiscovery) = checkLiquidityPositionsAfter("sell 600000");
// (ethFloor, ethAnchor, ethDiscovery, harbergFloor, harbergAnchor, harbergDiscovery) = checkLiquidityPositionsAfter("sell 600000");
// assertGt(ethFloor, 0, "Floor should still have ETH after manipulation");
// assertGt(harbDiscovery, 0, "Discovery should have increased HARB after buys");
// assertGt(harbergDiscovery, 0, "Discovery should have increased HARB after buys");
// slide();
// // Check liquidity positions after slide
// (ethFloor, ethAnchor, ethDiscovery, harbFloor, harbAnchor, harbDiscovery) = checkLiquidityPositionsAfter("slide");
// (ethFloor, ethAnchor, ethDiscovery, harbergFloor, harbergAnchor, harbergDiscovery) = checkLiquidityPositionsAfter("slide");
// // Ensure liquidity positions are as expected
// assertGt(ethFloor, 0, "Floor should still have ETH after manipulation");
// assertEq(harbFloor, 0, "Floor should have no HARB");
// assertEq(harbergFloor, 0, "Floor should have no HARB");
// assertEq(ethDiscovery, 0, "Discovery should not have ETH after slide");
// assertGt(harbDiscovery, 0, "Discovery should have increased HARB after buys");
// assertGt(harbergDiscovery, 0, "Discovery should have increased HARB after buys");
// writeCsv();
// }
@ -440,7 +482,7 @@ contract LiquidityManagerTest is Test {
// //revert();
// sell(harb.balanceOf(account));
// sell(harberg.balanceOf(account));
// slide(true);
@ -478,7 +520,7 @@ contract LiquidityManagerTest is Test {
// shift();
// sell(harb.balanceOf(account));
// sell(harberg.balanceOf(account));
// slide(true);
@ -508,20 +550,20 @@ contract LiquidityManagerTest is Test {
uint8 f = 0;
for (uint i = 0; i < numActions; i++) {
uint256 amount = (uint256(amounts[i]) * 1 ether) + 1 ether;
uint256 harbBal = harb.balanceOf(account);
if (harbBal == 0) {
uint256 harbergBal = harberg.balanceOf(account);
if (harbergBal == 0) {
amount = amount % (weth.balanceOf(account) / 2);
amount = amount == 0 ? weth.balanceOf(account) : amount;
buy(amount);
} else if (weth.balanceOf(account) == 0) {
sell(amount % harbBal);
sell(amount % harbergBal);
} else {
if (amount % 2 == 0) {
amount = amount % (weth.balanceOf(account) / 2);
amount = amount == 0 ? weth.balanceOf(account) : amount;
buy(amount);
} else {
sell(amount % harbBal);
sell(amount % harbergBal);
}
}
@ -551,7 +593,7 @@ contract LiquidityManagerTest is Test {
}
// Simulate large sell to push price down to floor
sell(harb.balanceOf(account));
sell(harberg.balanceOf(account));
slide(true);

View file

@ -4,31 +4,31 @@ pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
import "../src/Harb.sol";
import "../src/Harberg.sol";
import {TooMuchSnatch, Stake} from "../src/Stake.sol";
contract StakeTest is Test {
TwabController tc;
Harb harb;
Harberg harberg;
Stake stakingPool;
address liquidityPool;
address liquidityManager;
address taxPool;
event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 harbDeposit, uint256 share, uint32 taxRate);
event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 harbPayout);
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 {
tc = new TwabController(60 * 60, uint32(block.timestamp));
harb = new Harb("HARB", "HARB", tc);
taxPool = harb.TAX_POOL();
stakingPool = new Stake(address(harb));
harb.setStakingPool(address(stakingPool));
harberg = new Harberg("HARB", "HARB", tc);
taxPool = harberg.TAX_POOL();
stakingPool = new Stake(address(harberg));
harberg.setStakingPool(address(stakingPool));
liquidityPool = makeAddr("liquidityPool");
harb.setLiquidityPool(liquidityPool);
harberg.setLiquidityPool(liquidityPool);
liquidityManager = makeAddr("liquidityManager");
harb.setLiquidityManager(liquidityManager);
harberg.setLiquidityManager(liquidityManager);
}
@ -38,14 +38,14 @@ contract StakeTest is Test {
address staker = makeAddr("staker");
vm.startPrank(liquidityManager);
harb.mint(stakeAmount * 5);
harb.transfer(staker, stakeAmount);
harberg.mint(stakeAmount * 5);
harberg.transfer(staker, stakeAmount);
vm.stopPrank();
vm.startPrank(staker);
// Approve and stake
harb.approve(address(stakingPool), stakeAmount);
harberg.approve(address(stakingPool), stakeAmount);
uint256[] memory empty;
uint256 sharesExpected = stakingPool.assetsToShares(stakeAmount);
vm.expectEmit(address(stakingPool));
@ -69,13 +69,13 @@ contract StakeTest is Test {
address staker = makeAddr("staker");
vm.startPrank(liquidityManager);
harb.mint(stakeAmount * 5); // Ensuring the staker has enough balance
harb.transfer(staker, stakeAmount);
harberg.mint(stakeAmount * 5); // Ensuring the staker has enough balance
harberg.transfer(staker, stakeAmount);
vm.stopPrank();
// Staker stakes tokens
vm.startPrank(staker);
harb.approve(address(stakingPool), stakeAmount);
harberg.approve(address(stakingPool), stakeAmount);
uint256[] memory empty;
uint256 positionId = stakingPool.snatch(stakeAmount, staker, 1, empty);
@ -94,7 +94,7 @@ contract StakeTest is Test {
stakingPool.exitPosition(positionId);
// Check results after unstaking
assertEq(harb.balanceOf(staker), assetsAfterTax, "Assets after tax not returned correctly");
assertEq(harberg.balanceOf(staker), assetsAfterTax, "Assets after tax not returned correctly");
assertEq(stakingPool.outstandingStake(), 0, "Outstanding stake not updated correctly");
// Ensure the position is cleared
@ -115,10 +115,10 @@ contract StakeTest is Test {
// Mint and distribute tokens
vm.startPrank(liquidityManager);
harb.mint((initialStake1 + initialStake2) * 5);
harb.transfer(firstStaker, initialStake1);
harb.transfer(secondStaker, initialStake2);
harb.transfer(newStaker, snatchAmount);
harberg.mint((initialStake1 + initialStake2) * 5);
harberg.transfer(firstStaker, initialStake1);
harberg.transfer(secondStaker, initialStake2);
harberg.transfer(newStaker, snatchAmount);
vm.stopPrank();
// Setup initial stakers
@ -127,7 +127,7 @@ contract StakeTest is Test {
// Snatch setup
vm.startPrank(newStaker);
harb.approve(address(stakingPool), snatchAmount);
harberg.approve(address(stakingPool), snatchAmount);
uint256 snatchShares = stakingPool.assetsToShares(snatchAmount);
uint256[] memory targetPositions = new uint256[](2);
targetPositions[0] = positionId1;
@ -148,7 +148,7 @@ contract StakeTest is Test {
function setupStaker(address staker, uint256 amount, uint32 taxRate) private returns (uint256 positionId) {
vm.startPrank(staker);
harb.approve(address(stakingPool), amount);
harberg.approve(address(stakingPool), amount);
uint256[] memory empty;
positionId = stakingPool.snatch(amount, staker, taxRate, empty);
vm.stopPrank();
@ -172,16 +172,16 @@ contract StakeTest is Test {
function testRevert_SharesTooLow() public {
address staker = makeAddr("staker");
vm.startPrank(liquidityManager);
harb.mint(10 ether);
uint256 tooSmallStake = harb.previousTotalSupply() / 4000; // Less than minStake calculation
harb.transfer(staker, tooSmallStake);
harberg.mint(10 ether);
uint256 tooSmallStake = harberg.previousTotalSupply() / 4000; // Less than minStake calculation
harberg.transfer(staker, tooSmallStake);
vm.stopPrank();
vm.startPrank(staker);
harb.approve(address(stakingPool), tooSmallStake);
harberg.approve(address(stakingPool), tooSmallStake);
uint256[] memory empty;
vm.expectRevert(abi.encodeWithSelector(Stake.StakeTooLow.selector, staker, tooSmallStake, harb.previousTotalSupply() / 3000));
vm.expectRevert(abi.encodeWithSelector(Stake.StakeTooLow.selector, staker, tooSmallStake, harberg.previousTotalSupply() / 3000));
stakingPool.snatch(tooSmallStake, staker, 1, empty);
vm.stopPrank();
}
@ -190,16 +190,16 @@ contract StakeTest is Test {
address existingStaker = makeAddr("existingStaker");
address newStaker = makeAddr("newStaker");
vm.startPrank(liquidityManager);
harb.mint(10 ether);
harb.transfer(existingStaker, 1 ether);
harb.transfer(newStaker, 1 ether);
harberg.mint(10 ether);
harberg.transfer(existingStaker, 1 ether);
harberg.transfer(newStaker, 1 ether);
vm.stopPrank();
uint256 positionId = setupStaker(existingStaker, 1 ether, 5); // Existing staker with tax rate 5
vm.startPrank(newStaker);
harb.transfer(newStaker, 1 ether);
harb.approve(address(stakingPool), 1 ether);
harberg.transfer(newStaker, 1 ether);
harberg.approve(address(stakingPool), 1 ether);
uint256[] memory positions = new uint256[](1);
positions[0] = positionId; // Assuming position ID 1 has tax rate 5
@ -214,15 +214,15 @@ contract StakeTest is Test {
address ambitiousStaker = makeAddr("ambitiousStaker");
vm.startPrank(liquidityManager);
harb.mint(20 ether);
harb.transfer(staker, 2 ether);
harb.transfer(ambitiousStaker, 1 ether);
harberg.mint(20 ether);
harberg.transfer(staker, 2 ether);
harberg.transfer(ambitiousStaker, 1 ether);
vm.stopPrank();
uint256 positionId = setupStaker(staker, 2 ether, 10);
vm.startPrank(ambitiousStaker);
harb.approve(address(stakingPool), 1 ether);
harberg.approve(address(stakingPool), 1 ether);
uint256[] memory positions = new uint256[](1);
positions[0] = positionId;
@ -235,12 +235,12 @@ contract StakeTest is Test {
address staker = makeAddr("staker");
vm.startPrank(liquidityManager);
harb.mint(10 ether);
harb.transfer(staker, 1 ether);
harberg.mint(10 ether);
harberg.transfer(staker, 1 ether);
vm.stopPrank();
vm.startPrank(staker);
harb.approve(address(stakingPool), 1 ether);
harberg.approve(address(stakingPool), 1 ether);
uint256[] memory nonExistentPositions = new uint256[](1);
nonExistentPositions[0] = 999; // Assumed non-existent position ID
@ -256,12 +256,12 @@ contract StakeTest is Test {
address staker = makeAddr("staker");
vm.startPrank(liquidityManager);
harb.mint(10 ether);
harb.transfer(staker, 1 ether);
harberg.mint(10 ether);
harberg.transfer(staker, 1 ether);
vm.stopPrank();
vm.startPrank(staker);
harb.approve(address(stakingPool), 1 ether);
harberg.approve(address(stakingPool), 1 ether);
uint256[] memory empty;
uint256 positionId = stakingPool.snatch(1 ether, staker, 1, empty);
@ -287,12 +287,12 @@ contract StakeTest is Test {
address staker = makeAddr("staker");
vm.startPrank(liquidityManager);
harb.mint(10 ether);
harb.transfer(staker, 1 ether);
harberg.mint(10 ether);
harberg.transfer(staker, 1 ether);
vm.stopPrank();
vm.startPrank(staker);
harb.approve(address(stakingPool), 1 ether);
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);
@ -327,12 +327,12 @@ contract StakeTest is Test {
address staker = makeAddr("staker");
vm.startPrank(liquidityManager);
harb.mint(10 ether);
harb.transfer(staker, 1 ether);
harberg.mint(10 ether);
harberg.transfer(staker, 1 ether);
vm.stopPrank();
vm.startPrank(staker);
harb.approve(address(stakingPool), 1 ether);
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
vm.warp(block.timestamp + 365 days); // Move time forward to ensure maximum tax due