feature/simulations (#11)

this pull request:
- creates a unit test that can take any scenario file (default: `out/scenario.json` and play it back on the deployment
- during the playback a debug trace generated in `timeSeries.csv`
- extracts the sentimenter into a separate upgradeable contract

Co-authored-by: JulesCrown <admin@noip.localhost>
Co-authored-by: giteadmin <gite@admin.com>
Reviewed-on: http://gitea.loseyourip.com:4000/dark-meme-society/harb/pulls/11
This commit is contained in:
giteadmin 2024-11-07 15:33:40 +00:00
parent 38e1b65b94
commit bb34d0725f
15 changed files with 1159 additions and 451 deletions

5
onchain/.gitignore vendored
View file

@ -13,5 +13,8 @@ docs/
# Dotenv file
.env
.secret
.swp
/broadcast/
/broadcast/
tags

View file

@ -110,6 +110,10 @@ address: 0xCc7467616bBDB574D04C7e9d2B0982c59F33D43c
## Deployment on Base Sepolia
### Multisig
address: 0xf6a3eef9088A255c32b6aD2025f83E57291D9011
### Harberg
address: 0x54838DC097E7fC4736B801bF1c1FCf1597348265
@ -130,3 +134,40 @@ open features:
todos:
- write unit test for capital exit function
- would anchorLiquidityShare affect capitalInefficiency?
- could the UBI pool run dry?
- what happens if discovery runs out?
## Simulation data:
```json
{ "VWAP":0,
"comEthBal":1234,
"comHarbBal":0,
"comStakeShare":0,
"liquidity":[{
"liquidity":1234,
"tickLower":-123,
"tickUpper":124
}],
"startTime":1234,
"txns":[{
"action":5,"timeOffset":0,"x":0,"y":""},
{"action":0,"ethAmount":1,"x":0,"y":""},
{"action":5,"timeOffset":0,"x":0,"y":""},
{"action":0,"ethAmount":2,"x":0,"y":""},
{"action":5,"timeOffset":0,"x":0,"y":""},
{"action":0,"ethAmount":4,"x":0,"y":""},
{"action":2,"harbAmount":3000,"tax":10,"y":""},
{"action":5,"timeOffset":0,"x":0,"y":""},
{"action":4,"positionId":654321,"x":0,"y":""},
{"action":2,"harbAmount":5000,"tax":20,"y":""},
{"action":0,"ethAmount":8,"x":0,"y":""},
{"action":5,"timeOffset":0,"x":0,"y":""},
{"action":1,"harbAmount":20000,"x":0,"y":""},
{"action":5,"timeOffset":0,"x":0,"y":""},
{"action":4,"positionId":654321,"x":0,"y":""},
{"action":2,"harbAmount":8000,"tax":29,"y":""}
]
}
```

View file

@ -6,57 +6,27 @@ import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "../src/Harberg.sol";
import "../src/Stake.sol";
import "../src/Sentimenter.sol";
import "../src/helpers/UniswapHelpers.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
uint24 constant FEE = uint24(10_000);
contract DeployScript is Script {
using UniswapHelpers for IUniswapV3Pool;
bool token0isWeth;
address feeDest;
address weth;
address v3Factory;
address twabc;
function sqrt(uint256 y) internal pure returns (uint256 z) {
if (y > 3) {
z = y;
uint256 x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
// z is now the integer square root of y, or the closest integer to the square root of y.
}
function initializePoolFor1Cent(address _pool) public {
uint256 price;
if (token0isWeth) {
// ETH as token0, so we are setting the price of 1 ETH in terms of token1 (USD cent)
price = 3000 * 10**20; // 1 ETH = 3700 USD, scaled by 10^18 for precision
} else {
// Token (valued at 1 USD cent) as token0, ETH as token1
// We invert the logic to represent the price of 1 token in terms of ETH
price = uint256(10**16) / 3700; // Adjust for 18 decimal places
}
uint160 sqrtPriceX96 = uint160(sqrt(price) * 2**96 / 10**9); // Adjust sqrt value to 96-bit precision
console.log(uint160(sqrt(3000 * 10**20) * 2**96 / 10**9));
// Initialize pool with the calculated sqrtPriceX96
IUniswapV3Pool(_pool).initialize(sqrtPriceX96);
}
function run() public {
string memory seedPhrase = vm.readFile(".secret");
uint256 privateKey = vm.deriveKey(seedPhrase, 0);
vm.startBroadcast(privateKey);
address sender = vm.addr(privateKey);
console.log(sender);
TwabController tc;
if (twabc == address(0)) {
@ -71,9 +41,12 @@ contract DeployScript is Script {
harb.setStakingPool(address(stake));
IUniswapV3Factory factory = IUniswapV3Factory(v3Factory);
address liquidityPool = factory.createPool(weth, address(harb), FEE);
initializePoolFor1Cent(liquidityPool);
IUniswapV3Pool(liquidityPool).initializePoolFor1Cent(token0isWeth);
harb.setLiquidityPool(liquidityPool);
LiquidityManager liquidityManager = new LiquidityManager(v3Factory, weth, address(harb));
Sentimenter sentimenter = new Sentimenter();
bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(harb),address(stake));
ERC1967Proxy proxy = new ERC1967Proxy(address(sentimenter), 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));

View file

@ -14,7 +14,7 @@ import {Math} from "@openzeppelin/utils/math/Math.sol";
import {ABDKMath64x64} from "@abdk/ABDKMath64x64.sol";
import "./interfaces/IWETH9.sol";
import {Harberg} from "./Harberg.sol";
import {Sentimenter} from "./Sentimenter.sol";
/**
* @title LiquidityManager for Harberg Token on Uniswap V3
@ -42,14 +42,6 @@ contract LiquidityManager {
uint128 internal constant DISCOVERY_DEPTH = 200; // 500 // 500%
// only working with UNI V3 1% fee tier pools
uint24 internal constant FEE = uint24(10_000);
// ANCHOR_LIQ_SHARE is the mininum share of total ETH in control
// that will be left to put into anchor positon.
uint256 internal constant MIN_ANCHOR_LIQ_SHARE = 5; // 5 = 5%
uint256 internal constant MAX_ANCHOR_LIQ_SHARE = 25;
// virtual liabilities that are added to push the calculated floor price down artificially,
// creating a security margin for attacks on liquidity
uint256 internal constant MIN_CAPITAL_INEFFICIENCY = 70;
uint256 internal constant MAX_CAPITAL_INEFFICIENCY = 200;
// used to double-check price with uni oracle
uint32 internal constant PRICE_STABILITY_INTERVAL = 300; // 5 minutes in seconds
int24 internal constant MAX_TICK_DEVIATION = 50; // how much is that?
@ -58,6 +50,7 @@ contract LiquidityManager {
address private immutable factory;
IWETH9 private immutable weth;
Harberg private immutable harb;
Sentimenter private immutable sentimenter;
IUniswapV3Pool private immutable pool;
bool private immutable token0isWeth;
PoolKey private poolKey;
@ -76,16 +69,12 @@ contract LiquidityManager {
mapping(Stage => TokenPosition) public positions;
// the address where liquidity fees will be sent
address public feeDestination;
// the minimum share of ETH that will be put into the anchor
uint256 public anchorLiquidityShare;
// the higher the inefficiency, the more conservative the positioning of floor
uint256 public capitalInefficiency;
error ZeroAddressInSetter();
error AddressAlreadySet();
event EthScarcity(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 capitalInefficiency, uint256 anchorLiquidityShare, int24 vwapTick);
event EthAbundance(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 capitalInefficiency, uint256 anchorLiquidityShare, int24 vwapTick);
event EthScarcity(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 sentiment, int24 vwapTick);
event EthAbundance(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 sentiment, int24 vwapTick);
/// @dev Function modifier to ensure that the caller is the feeDestination
modifier onlyFeeDestination() {
@ -98,15 +87,14 @@ contract LiquidityManager {
/// @param _WETH9 The address of the WETH contract for handling ETH in trades.
/// @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) {
constructor(address _factory, address _WETH9, address _harb, address _sentimenter) {
factory = _factory;
weth = IWETH9(_WETH9);
poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE);
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
harb = Harberg(_harb);
token0isWeth = _WETH9 < _harb;
anchorLiquidityShare = MAX_ANCHOR_LIQ_SHARE;
capitalInefficiency = MIN_CAPITAL_INEFFICIENCY; // starting at 95% fuzzer passes tests
sentimenter = Sentimenter(_sentimenter);
}
/// @notice Callback function that Uniswap V3 calls for liquidity actions requiring minting or burning of tokens.
@ -137,18 +125,6 @@ contract LiquidityManager {
feeDestination = feeDestination_;
}
function setAnchorLiquidityShare(uint256 anchorLiquidityShare_) external onlyFeeDestination {
require(anchorLiquidityShare_ >= MIN_ANCHOR_LIQ_SHARE, "anchor liquidity share too low.");
require(anchorLiquidityShare_ <= MAX_ANCHOR_LIQ_SHARE, "anchor liquidity share too high.");
anchorLiquidityShare = anchorLiquidityShare_;
}
function setCapitalInfefficiency(uint256 capitalInefficiency_) external onlyFeeDestination {
require(capitalInefficiency_ >= MIN_CAPITAL_INEFFICIENCY, "capital inefficiency is too low.");
require(capitalInefficiency_ <= MAX_CAPITAL_INEFFICIENCY, "capital inefficiency is too high.");
capitalInefficiency = capitalInefficiency_;
}
function setMinStakeSupplyFraction(uint256 mssf_) external onlyFeeDestination {
harb.setMinStakeSupplyFraction(mssf_);
}
@ -231,15 +207,18 @@ contract LiquidityManager {
/// @notice Internal function to set or adjust the floor, anchor, and discovery positions based on current market conditions and the manager's strategy.
/// @param currentTick The current market tick.
/// @dev Recalculates and realigns all liquidity positions according to the latest market data and strategic requirements.
function _set(int24 currentTick) internal {
function _set(int24 currentTick, uint256 sentiment) internal {
// estimate the lower tick of the anchor
int24 vwapTick;
uint256 outstandingSupply = harb.outstandingSupply();
uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this)));
uint256 floorEthBalance = ethBalance * (100 - anchorLiquidityShare) / 100;
// this enforces an floor liquidity share of 75% to 95 %;
uint256 floorEthBalance = (3 * ethBalance / 4) + (2 * sentiment * ethBalance / 10**19);
if (outstandingSupply > 0) {
vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInefficiency / 100 , floorEthBalance);
// this enables a "capital inefficiency" of 70% to 170%;
uint256 balancedCapital = (7 * outstandingSupply / 10) + (outstandingSupply * sentiment / 10**18);
vwapTick = tickAtPrice(token0isWeth, balancedCapital, floorEthBalance);
} else {
vwapTick = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - ANCHOR_SPACING;
}
@ -311,15 +290,17 @@ contract LiquidityManager {
uint256 vwapX96 = 0;
uint256 requiredEthForBuyback = 0;
if (cumulativeVolume > 0) {
vwapX96 = cumulativeVolumeWeightedPriceX96 * capitalInefficiency / 100 / cumulativeVolume; // in harb/eth
vwapX96 = cumulativeVolumeWeightedPriceX96 / cumulativeVolume; // in harb/eth
vwapX96 = (7 * vwapX96 / 10) + (vwapX96 * sentiment / 10**18);
requiredEthForBuyback = outstandingSupply.mulDiv(vwapX96, (1 << 96));
}
// make a new calculation of the vwapTick, having updated outstandingSupply
if (floorEthBalance < requiredEthForBuyback) {
// not enough ETH, find a lower price
requiredEthForBuyback = floorEthBalance;
vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInefficiency / 100 , requiredEthForBuyback);
emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInefficiency, anchorLiquidityShare, vwapTick);
uint256 balancedCapital = (7 * outstandingSupply / 10) + (outstandingSupply * sentiment / 10**18);
vwapTick = tickAtPrice(token0isWeth, balancedCapital , requiredEthForBuyback);
emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, sentiment, vwapTick);
} else if (vwapX96 == 0) {
requiredEthForBuyback = floorEthBalance;
vwapTick = currentTick;
@ -328,7 +309,7 @@ contract LiquidityManager {
vwapTick = tickAtPriceRatio(int128(int256(vwapX96 >> 32)));
// convert to pool tick
vwapTick = token0isWeth ? -vwapTick : vwapTick;
emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInefficiency, anchorLiquidityShare, vwapTick);
emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, sentiment, vwapTick);
}
// move floor below anchor, if needed
if (token0isWeth) {
@ -451,7 +432,7 @@ contract LiquidityManager {
/// @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 {
function recenter() external returns (bool isUp, uint256 sentiment) {
if (recenterAccess != address(0)) {
require(msg.sender == recenterAccess, "access denied");
}
@ -460,7 +441,7 @@ contract LiquidityManager {
// check slippage with oracle
require(_isPriceStable(currentTick), "price deviated from oracle");
bool isUp = false;
isUp = false;
// check how price moved
if (positions[Stage.ANCHOR].liquidity > 0) {
// get the anchor position
@ -484,8 +465,16 @@ contract LiquidityManager {
if (isUp) {
harb.setPreviousTotalSupply(harb.totalSupply());
}
try sentimenter.getSentiment() returns (uint256 currentSentiment) {
sentiment = (currentSentiment > 10**18) ? 10**18 : currentSentiment;
} catch {
//sentiment = 10**18 / 2;
sentiment = 0;
}
// set new positions
_set(currentTick);
_set(currentTick, sentiment);
}
}

View file

@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import {Harberg} from "./Harberg.sol";
import {Stake} from "./Stake.sol";
import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol";
contract Sentimenter is Initializable, UUPSUpgradeable {
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);
}
/**
* @dev Throws if called by any account other than the admin.
*/
modifier onlyAdmin() {
_checkAdmin();
_;
}
/**
* @dev Throws if the sender is not the admin.
*/
function _checkAdmin() internal view virtual {
if (_getAdmin() != msg.sender) {
revert UnauthorizedAccount(msg.sender);
}
}
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {}
function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked) public pure returns (uint256 sentimentValue) {
uint256 deltaS = 10**18 - percentageStaked;
if (percentageStaked > 92 * 10**16) {
// Rapid drop for high percentageStaked values
uint256 penalty = (deltaS * deltaS * deltaS * averageTaxRate) / (20 * 10**48);
sentimentValue = penalty / 2;
} else {
// Linearly decreasing sentiment value with rising percentageStaked
uint256 baseSentiment = 10**18 - (percentageStaked * 10**18) / (92 * 10**16); // Decreases from 10**18 to 0
// Apply penalty based on averageTaxRate
if (averageTaxRate <= 10**16) {
sentimentValue = baseSentiment; // No penalty for low averageTaxRate
} else if (averageTaxRate <= 5 * 10**16) {
uint256 ratePenalty = (averageTaxRate - 10**16) * baseSentiment / (4 * 10**16);
sentimentValue = baseSentiment > ratePenalty ? baseSentiment - ratePenalty : 0;
} else {
sentimentValue = 10**18; // High averageTaxRate results in maximum sentiment value (low sentiment)
}
}
return sentimentValue;
}
/// @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 returns (uint256 sentiment) {
uint256 percentageStaked = stake.getPercentageStaked();
uint256 averageTaxRate = stake.getAverageTaxRate();
sentiment = calculateSentiment(averageTaxRate, percentageStaked);
}
}

View file

@ -60,7 +60,7 @@ contract Stake {
address owner;
uint32 creationTime;
uint32 lastTaxTime;
uint32 taxRate; // e.g. value of 60 = 60% tax per year
uint32 taxRate; // index of TAX_RATES array
}
Harberg private immutable harberg;
@ -72,6 +72,9 @@ contract Stake {
mapping(uint256 => StakingPosition) public positions;
// Array to keep track of total shares at each tax rate
uint256[] public totalSharesAtTaxRate;
/// @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.
@ -81,6 +84,8 @@ contract Stake {
taxPool = Harberg(_harberg).TAX_POOL();
// start counting somewhere
nextPositionId = 654321;
// Initialize totalSharesAtTaxRate array
totalSharesAtTaxRate = new uint256[](TAX_RATES.length);
}
function authorizedStake() private view returns (uint256) {
@ -104,12 +109,15 @@ contract Stake {
if (assetsBefore - taxAmountDue > 0) {
// if something left over, update storage
uint256 shareAfterTax = assetsToShares(assetsBefore - taxAmountDue);
outstandingStake -= pos.share - shareAfterTax;
uint256 deltaShare = pos.share - shareAfterTax;
totalSharesAtTaxRate[pos.taxRate] -= deltaShare;
outstandingStake -= deltaShare;
pos.share = shareAfterTax;
pos.lastTaxTime = uint32(block.timestamp);
emit PositionTaxPaid(positionId, pos.owner, taxAmountDue, shareAfterTax, pos.taxRate);
} else {
// if nothing left over, liquidate position
totalSharesAtTaxRate[pos.taxRate] -= pos.share;
outstandingStake -= pos.share;
emit PositionTaxPaid(positionId, pos.owner, taxAmountDue, 0, pos.taxRate);
emit PositionRemoved(positionId, pos.owner, 0);
@ -122,6 +130,7 @@ contract Stake {
/// @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 {
totalSharesAtTaxRate[pos.taxRate] -= pos.share;
outstandingStake -= pos.share;
address owner = pos.owner;
uint256 assets = sharesToAssets(pos.share);
@ -137,6 +146,7 @@ contract Stake {
require (sharesToTake < pos.share, "position too small");
uint256 assets = sharesToAssets(sharesToTake);
pos.share -= sharesToTake;
totalSharesAtTaxRate[pos.taxRate] -= sharesToTake;
outstandingStake -= sharesToTake;
emit PositionShrunk(positionId, pos.owner, pos.share, assets);
SafeERC20.safeTransfer(harberg, pos.owner, assets);
@ -252,6 +262,7 @@ contract Stake {
sp.creationTime = uint32(block.timestamp);
sp.taxRate = taxRate;
totalSharesAtTaxRate[taxRate] += sharesWanted;
outstandingStake += sharesWanted;
emit PositionCreated(positionId, sp.owner, assets, sp.share, sp.taxRate);
}
@ -299,6 +310,8 @@ contract Stake {
// to prevent snatch-and-change grieving attack, pay TAX_FLOOR_DURATION
require(taxRate > pos.taxRate, "tax too low to snatch");
_payTax(positionId, pos, 0);
totalSharesAtTaxRate[pos.taxRate] -= pos.share;
totalSharesAtTaxRate[taxRate] += pos.share;
pos.taxRate = taxRate;
emit PositionRateHiked(positionId, pos.owner, taxRate);
}
@ -345,5 +358,25 @@ contract Stake {
amountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
}
/// @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];
}
}
/// @notice Computes the percentage of Harberg staked from outstanding Stake and authorized Stake.
/// @return percentageStaked A number between 0 and 1e18 indicating the percentage of Harberg supply staked.
function getPercentageStaked() external view returns (uint256 percentageStaked) {
percentageStaked = (outstandingStake * 1e18) / authorizedStake();
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,128 @@
// SPDX-License-Identifier: GPL-3.0-or-later
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/Harberg.sol";
import {TooMuchSnatch, Stake} from "../src/Stake.sol";
import "../src/Sentimenter.sol";
import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import {MockSentimenter} from "./mocks/MockSentimenter.sol";
contract SentimenterTest is Test {
TwabController tc;
Harberg harberg;
Stake stake;
Sentimenter sentimenter;
address liquidityPool;
address liquidityManager;
address taxPool;
function setUp() public {
tc = new TwabController(60 * 60, uint32(block.timestamp));
harberg = new Harberg("HARB", "HARB", tc);
taxPool = harberg.TAX_POOL();
stake = new Stake(address(harberg));
harberg.setStakingPool(address(stake));
liquidityPool = makeAddr("liquidityPool");
harberg.setLiquidityPool(liquidityPool);
liquidityManager = makeAddr("liquidityManager");
harberg.setLiquidityManager(liquidityManager);
// deploy upgradeable tuner contract
Sentimenter _sentimenter = new Sentimenter();
bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(harberg),address(stake));
ERC1967Proxy proxy = new ERC1967Proxy(address(_sentimenter), params);
sentimenter = Sentimenter(address(proxy));
}
function doSnatch(address staker, uint256 amount, uint32 taxRate) private returns (uint256 positionId) {
vm.startPrank(staker);
harberg.approve(address(stake), amount);
uint256[] memory empty;
positionId = stake.snatch(amount, staker, taxRate, empty);
vm.stopPrank();
}
function testSentiment() public {
uint256 smallstake = 0.3e17;
uint256 stakeOneThird = 1 ether;
uint256 stakeTwoThird = 2 ether;
address staker = makeAddr("staker");
// Mint and distribute tokens
vm.startPrank(liquidityManager);
// mint all the tokens we will need in the test
harberg.mint((smallstake + stakeOneThird + stakeTwoThird) * 5);
// send 20% of that to staker
harberg.transfer(staker, (smallstake + stakeOneThird + stakeTwoThird) * 2);
vm.stopPrank();
// Setup initial stakers
uint256 positionId1 = doSnatch(staker, smallstake, 0);
uint256 sentiment;
sentiment = sentimenter.getSentiment();
// 0.99 - horrible sentiment
assertApproxEqRel(sentiment, 9.9e17, 1e16);
vm.prank(staker);
stake.exitPosition(positionId1);
uint256 positionId2 = doSnatch(staker, stakeOneThird, 2);
sentiment = sentimenter.getSentiment();
// 0.64 - depressive sentiment
assertApproxEqRel(sentiment, 6.4e17, 1e16);
vm.prank(staker);
stake.exitPosition(positionId2);
positionId1 = doSnatch(staker, stakeOneThird, 10);
positionId2 = doSnatch(staker, stakeTwoThird, 11);
sentiment = sentimenter.getSentiment();
// 0.00018 - feaking good sentiment
assertApproxEqRel(sentiment, 1.8e14, 1e17);
vm.startPrank(staker);
stake.exitPosition(positionId1);
stake.exitPosition(positionId2);
vm.stopPrank();
positionId1 = doSnatch(staker, stakeOneThird, 29);
positionId2 = doSnatch(staker, stakeTwoThird, 29);
sentiment = sentimenter.getSentiment();
// 0.024 - pretty good sentiment
assertApproxEqRel(sentiment, 2.4e16, 2e16);
vm.startPrank(staker);
stake.exitPosition(positionId1);
stake.exitPosition(positionId2);
vm.stopPrank();
positionId2 = doSnatch(staker, stakeTwoThird, 15);
sentiment = sentimenter.getSentiment();
// 0.17 - positive sentiment
assertApproxEqRel(sentiment, 1.7e17, 2e16);
vm.startPrank(staker);
stake.exitPosition(positionId2);
vm.stopPrank();
positionId1 = doSnatch(staker, stakeOneThird, 15);
sentiment = sentimenter.getSentiment();
// 0.4 - OK sentiment
assertApproxEqRel(sentiment, 3.9e17, 2e16);
}
function testContractUpgrade() public {
address newSent = address(new MockSentimenter());
sentimenter.upgradeTo(newSent);
uint256 sentiment = sentimenter.getSentiment();
assertEq(sentiment, 1234567890123456789, "should have been upgraded");
}
}

View file

@ -0,0 +1,230 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import "../src/interfaces/IWETH9.sol";
import {WETH} from "solmate/tokens/WETH.sol";
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 {Harberg} from "../src/Harberg.sol";
import "../src/helpers/UniswapHelpers.sol";
import {Stake, ExceededAvailableStake} from "../src/Stake.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
import {UniswapTestBase} from "./helpers/UniswapTestBase.sol";
import {CSVHelper} from "./helpers/CSVHelper.sol";
import {CSVManager} from "./helpers/CSVManager.sol";
import "../src/Sentimenter.sol";
address constant TAX_POOL = address(2);
// default fee of 1%
uint24 constant FEE = uint24(10_000);
contract SimulationsTest is UniswapTestBase, CSVManager {
using UniswapHelpers for IUniswapV3Pool;
using CSVHelper for *;
IUniswapV3Factory factory;
Stake stakingPool;
LiquidityManager lm;
address feeDestination = makeAddr("fees");
uint256 supplyOnRecenter;
uint256 timeOnRecenter;
int256 supplyChange;
struct Position {
uint256 liquidity;
int32 tickLower;
int32 tickUpper;
}
enum ActionType {
Buy,
Sell,
Snatch,
Unstake,
PayTax,
Recenter,
Mint,
Burn
}
struct Action {
uint256 kind; // buy, sell, snatch, unstake, paytax, recenter, mint, burn
uint256 amount1; // x , x , x , x , x , x , ,
uint256 amount2; // , , x , , , , ,
string position; // , , x , , , , ,
}
struct Scenario {
uint256 VWAP;
uint256 comEthBal;
uint256 comHarbBal;
uint256 comStakeShare;
Position[] liquidity; // the positions are floor, anchor, liquidity, [comPos1, comPos2 ...]
uint256 time;
Action[] txns;
}
function setUp() public {
factory = UniswapHelpers.deployUniswapFactory();
weth = IWETH9(address(new WETH()));
TwabController tc = new TwabController(60 * 60, uint32(block.timestamp));
harberg = new Harberg("Harberg", "HRB", tc);
pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE));
token0isWeth = address(weth) < address(harberg);
pool.initializePoolFor1Cent(token0isWeth);
stakingPool = new Stake(address(harberg));
harberg.setStakingPool(address(stakingPool));
Sentimenter senti = new Sentimenter();
senti.initialize(address(harberg), address(stakingPool));
lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(senti));
lm.setFeeDestination(feeDestination);
vm.prank(feeDestination);
harberg.setLiquidityManager(address(lm));
harberg.setLiquidityPool(address(pool));
vm.deal(address(lm), 1 ether);
timeOnRecenter = block.timestamp;
initializeTimeSeriesCSV();
}
function buy(uint256 amountEth) internal {
performSwap(amountEth, true);
}
function sell(uint256 amountHarb) internal {
performSwap(amountHarb, false);
}
receive() external payable {}
function writeCsv() public {
writeCSVToFile("./out/timeSeries.csv"); // Write CSV to file
}
function recenter() internal {
// have some time pass to record prices in uni oracle
uint256 timeBefore = block.timestamp;
vm.warp(timeBefore + 5 minutes);
// uint256
// uint256 supplyDelta = currentSupply - supplyOnRecenter;
// uint256 timeDelta = block.timestamp - timeOnRecenter;
// uint256 growthPerYear = supplyDelta * 52 weeks / timeDelta;
// // console.log("supplyOnLastRecenter");
// // console.log(supplyOnRecenter);
// // console.log("currentSupply");
// // console.log(currentSupply);
// uint256 growthPercentage = growthPerYear * 100 >= currentSupply ? 101 : growthPerYear * 100 / currentSupply;
// supplyOnRecenter = currentSupply;
timeOnRecenter = block.timestamp;
uint256 supplyBefore = harberg.totalSupply();
lm.recenter();
supplyChange = int256(harberg.totalSupply()) - int256(supplyBefore);
// TODO: update supplyChangeOnLastRecenter
// have some time pass to record prices in uni oracle
timeBefore = block.timestamp;
vm.warp(timeBefore + 5 minutes);
}
function getPriceInHarb(uint160 sqrtPriceX96) internal returns (uint256 price) {
uint256 sqrtPrice = uint256(sqrtPriceX96);
if (token0isWeth) {
// WETH is token0, price = (sqrtPriceX96 / 2^96)^2
price = (sqrtPrice * sqrtPrice) / (1 << 192);
} else {
// WETH is token1, price = (2^96 / sqrtPriceX96)^2
price = ((1 << 192) * 1e18) / (sqrtPrice * sqrtPrice);
}
}
function recordState() internal {
uint160 sqrtPriceX96;
uint256 outstandingStake = stakingPool.outstandingStake();
(sqrtPriceX96, , , , , , ) = pool.slot0();
string memory newRow = string(abi.encodePacked(CSVHelper.uintToStr(block.timestamp),
",", CSVHelper.uintToStr(getPriceInHarb(sqrtPriceX96)),
",", CSVHelper.uintToStr(harberg.totalSupply() / 1e18),
",", CSVHelper.intToStr(supplyChange / 1e18),
",", CSVHelper.uintToStr(outstandingStake * 500 / 1e25)
));
if (outstandingStake > 0) {
uint256 sentiment;
uint256 avgTaxRate;
//(sentiment, avgTaxRate) = stakingPool.getSentiment();
newRow = string.concat(newRow,
",", CSVHelper.uintToStr(avgTaxRate),
",", CSVHelper.uintToStr(sentiment),
",", CSVHelper.uintToStr(harberg.sumTaxCollected() / 1e18)
);
} else {
newRow = string.concat(newRow, ", 0, 100, 95, 25, 0");
}
appendCSVRow(newRow); // Append the new row to the CSV
}
function stake(uint256 harbAmount, uint32 taxRateIndex) internal returns (uint256) {
vm.startPrank(account);
harberg.approve(address(stakingPool), harbAmount);
uint256[] memory empty;
uint256 posId = stakingPool.snatch(harbAmount, account, taxRateIndex, empty);
vm.stopPrank();
return posId;
}
function unstake(uint256 positionId) internal {
vm.prank(account);
stakingPool.exitPosition(positionId);
}
function handleAction(Action memory action) internal {
if (action.kind == uint256(ActionType.Buy)) {
buy(action.amount1 * 10**18);
} else if (action.kind == uint256(ActionType.Sell)) {
sell(action.amount1 * 10**18);
} else if (action.kind == uint256(ActionType.Snatch)) {
stake(action.amount1 ** 10**18, uint32(action.amount2));
} else if (action.kind == uint256(ActionType.Unstake)) {
unstake(action.amount1);
} else if (action.kind == uint256(ActionType.PayTax)) {
stakingPool.payTax(action.amount1);
} else if (action.kind == uint256(ActionType.Recenter)) {
uint256 timeBefore = block.timestamp;
vm.warp(timeBefore + action.amount1);
recenter();
}
}
function testGeneration() public {
string memory json = vm.readFile("out/scenarios.json");
bytes memory data = vm.parseJson(json);
Scenario memory scenario = abi.decode(data, (Scenario));
vm.deal(account, scenario.comEthBal * 10**18);
vm.prank(account);
weth.deposit{value: address(account).balance}();
for (uint256 i = 0; i < scenario.txns.length; i++) {
handleAction(scenario.txns[i]);
recordState();
}
//writeCsv();
}
}

View file

@ -31,6 +31,20 @@ contract StakeTest is Test {
harberg.setLiquidityManager(liquidityManager);
}
function assertPosition(uint256 positionId, uint256 expectedShares, uint32 expectedTaxRate) private view {
(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 expectedInitialShares = stakingPool.assetsToShares(initialStake);
bool positionRemoved = remainingShare == 0;
bool positionShrunk = remainingShare < expectedInitialShares;
assertTrue(positionRemoved || positionShrunk, "Position was not correctly shrunk or removed");
}
function testBasicStaking() public {
// Setup
@ -122,8 +136,8 @@ contract StakeTest is Test {
vm.stopPrank();
// Setup initial stakers
uint256 positionId1 = setupStaker(firstStaker, initialStake1, 1);
uint256 positionId2 = setupStaker(secondStaker, initialStake2, 5);
uint256 positionId1 = doSnatch(firstStaker, initialStake1, 1);
uint256 positionId2 = doSnatch(secondStaker, initialStake2, 5);
// Snatch setup
vm.startPrank(newStaker);
@ -146,7 +160,7 @@ contract StakeTest is Test {
vm.stopPrank();
}
function setupStaker(address staker, uint256 amount, uint32 taxRate) private returns (uint256 positionId) {
function doSnatch(address staker, uint256 amount, uint32 taxRate) private returns (uint256 positionId) {
vm.startPrank(staker);
harberg.approve(address(stakingPool), amount);
uint256[] memory empty;
@ -154,21 +168,97 @@ contract StakeTest is Test {
vm.stopPrank();
}
function assertPosition(uint256 positionId, uint256 expectedShares, uint32 expectedTaxRate) private view {
(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 bp(uint256 val) internal pure returns (uint256) {
return val / 1e15;
}
function verifyPositionShrunkOrRemoved(uint256 positionId, uint256 initialStake) private view {
(uint256 remainingShare, , , , ) = stakingPool.positions(positionId);
uint256 expectedInitialShares = stakingPool.assetsToShares(initialStake);
bool positionRemoved = remainingShare == 0;
bool positionShrunk = remainingShare < expectedInitialShares;
assertTrue(positionRemoved || positionShrunk, "Position was not correctly shrunk or removed");
function denormTR(uint256 normalizedTaxRate) internal pure returns (uint256) {
return normalizedTaxRate * 97;
}
function testAvgTaxRateAndPercentageStaked() public {
uint256 smallstake = 0.3e17;
uint256 stakeOneThird = 1 ether;
uint256 stakeTwoThird = 2 ether;
address staker = makeAddr("staker");
// Mint and distribute tokens
vm.startPrank(liquidityManager);
// mint all the tokens we will need in the test
harberg.mint((smallstake + stakeOneThird + stakeTwoThird) * 5);
// send 20% of that to staker
harberg.transfer(staker, (smallstake + stakeOneThird + stakeTwoThird) * 2);
vm.stopPrank();
// Setup initial stakers
uint256 positionId1 = doSnatch(staker, smallstake, 0);
uint256 avgTaxRate;
uint256 percentageStaked;
avgTaxRate = stakingPool.getAverageTaxRate();
percentageStaked = stakingPool.getPercentageStaked();
// let this be about 10 basis points of tax rate
assertApproxEqRel(bp(denormTR(avgTaxRate)), 10, 1e17);
assertApproxEqRel(bp(percentageStaked), 10, 1e17);
vm.prank(staker);
stakingPool.exitPosition(positionId1);
uint256 positionId2 = doSnatch(staker, stakeOneThird, 2);
avgTaxRate = stakingPool.getAverageTaxRate();
percentageStaked = stakingPool.getPercentageStaked();
assertApproxEqRel(bp(denormTR(avgTaxRate)), 50, 1e17);
assertApproxEqRel(bp(percentageStaked), 300, 1e17);
vm.prank(staker);
stakingPool.exitPosition(positionId2);
positionId1 = doSnatch(staker, stakeOneThird, 10);
positionId2 = doSnatch(staker, stakeTwoThird, 11);
avgTaxRate = stakingPool.getAverageTaxRate();
percentageStaked = stakingPool.getPercentageStaked();
assertApproxEqRel(bp(denormTR(avgTaxRate)), 730, 1e17);
assertApproxEqRel(bp(percentageStaked), 1000, 1e17);
vm.startPrank(staker);
stakingPool.exitPosition(positionId1);
stakingPool.exitPosition(positionId2);
vm.stopPrank();
positionId1 = doSnatch(staker, stakeOneThird, 29);
positionId2 = doSnatch(staker, stakeTwoThird, 29);
avgTaxRate = stakingPool.getAverageTaxRate();
assertApproxEqRel(bp(denormTR(avgTaxRate)), 97000, 1e17);
vm.startPrank(staker);
stakingPool.exitPosition(positionId1);
stakingPool.exitPosition(positionId2);
vm.stopPrank();
positionId2 = doSnatch(staker, stakeTwoThird, 15);
avgTaxRate = stakingPool.getAverageTaxRate();
percentageStaked = stakingPool.getPercentageStaked();
assertApproxEqRel(bp(denormTR(avgTaxRate)), 2500, 1e17);
assertApproxEqRel(bp(percentageStaked), 660, 1e17);
vm.startPrank(staker);
stakingPool.exitPosition(positionId2);
vm.stopPrank();
positionId1 = doSnatch(staker, stakeOneThird, 15);
avgTaxRate = stakingPool.getAverageTaxRate();
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);
@ -195,7 +285,7 @@ contract StakeTest is Test {
harberg.transfer(newStaker, 1 ether);
vm.stopPrank();
uint256 positionId = setupStaker(existingStaker, 1 ether, 5); // Existing staker with tax rate 5
uint256 positionId = doSnatch(existingStaker, 1 ether, 5); // Existing staker with tax rate 5
vm.startPrank(newStaker);
harberg.transfer(newStaker, 1 ether);
@ -219,7 +309,7 @@ contract StakeTest is Test {
harberg.transfer(ambitiousStaker, 1 ether);
vm.stopPrank();
uint256 positionId = setupStaker(staker, 2 ether, 10);
uint256 positionId = doSnatch(staker, 2 ether, 10);
vm.startPrank(ambitiousStaker);
harberg.approve(address(stakingPool), 1 ether);

View file

@ -0,0 +1,90 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title CSVHelper
* @dev Library for managing CSV data in Solidity, including converting values to strings and writing CSV data.
*/
library CSVHelper {
/**
* @notice Creates a standard CSV header for liquidity position data.
* @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";
}
function createTimeSeriesHeader() internal pure returns (string memory) {
return "time, price, harbTotalSupply, supplyChange, stakeOutstandingShares, avgTaxRate, sentiment, taxCollected";
}
/**
* @notice Appends new CSV data to the existing CSV string.
* @param csv The current CSV string.
* @param newRow The new row to append.
* @return The updated CSV string.
*/
function appendRow(string memory csv, string memory newRow) internal pure returns (string memory) {
return string(abi.encodePacked(csv, "\n", newRow));
}
/**
* @notice Converts a `uint256` to a string.
* @param _i The integer to convert.
* @return The string representation of the integer.
*/
function uintToStr(uint256 _i) internal pure returns (string memory) {
if (_i == 0) {
return "0";
}
uint256 j = _i;
uint256 len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint256 k = len;
while (_i != 0) {
k = k - 1;
uint8 temp = (48 + uint8(_i - _i / 10 * 10));
bstr[k] = bytes1(temp);
_i /= 10;
}
return string(bstr);
}
/**
* @notice Converts an `int256` to a string.
* @param _i The integer to convert.
* @return The string representation of the integer.
*/
function intToStr(int256 _i) internal pure returns (string memory) {
if (_i == 0) {
return "0";
}
bool negative = _i < 0;
uint256 absValue = uint256(negative ? -_i : _i);
uint256 len;
uint256 j = absValue;
while (j != 0) {
len++;
j /= 10;
}
if (negative) {
len++; // Increase length for the minus sign.
}
bytes memory bstr = new bytes(len);
uint256 k = len;
while (absValue != 0) {
k = k - 1;
uint8 temp = (48 + uint8(absValue - absValue / 10 * 10));
bstr[k] = bytes1(temp);
absValue /= 10;
}
if (negative) {
bstr[0] = '-';
}
return string(bstr);
}
}

View file

@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "./CSVHelper.sol";
/**
* @title CSVManager
* @dev An abstract contract that manages CSV creation, row appending, and writing to file.
* Contracts that inherit this contract can use the CSV functionality without managing the CSV string directly.
*/
abstract contract CSVManager is Test {
using CSVHelper for *;
string internal csv;
/**
* @notice Creates the header for the CSV file.
* This function should be called in the `setUp` function of derived contracts.
*/
function initializePositionsCSV() internal {
csv = CSVHelper.createPositionsHeader();
}
function initializeTimeSeriesCSV() internal {
csv = CSVHelper.createTimeSeriesHeader();
}
/**
* @notice Appends a new row to the CSV string.
* @param newRow The new row to append.
*/
function appendCSVRow(string memory newRow) internal {
csv = CSVHelper.appendRow(csv, newRow);
}
/**
* @notice Writes the CSV string to a file.
* @param filePath The path where the CSV file will be saved.
*/
function writeCSVToFile(string memory filePath) internal {
vm.writeFile(filePath, csv);
}
}

View file

@ -0,0 +1,81 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
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.
*/
abstract contract UniswapTestBase is Test {
address account = makeAddr("alice");
IUniswapV3Pool public pool;
IWETH9 public weth;
Harberg public harberg;
bool public token0isWeth;
/**
* @dev Performs a swap in the Uniswap V3 pool.
* @param amount The amount to swap.
* @param isBuy True if buying WETH, false if selling.
*/
function performSwap(uint256 amount, bool isBuy) internal {
uint160 limit;
// Determine the swap direction
bool zeroForOne = isBuy ? token0isWeth : !token0isWeth;
if (isBuy) {
vm.prank(account);
weth.transfer(address(this), amount);
} else {
vm.prank(account);
harberg.approve(address(this), amount);
}
// Set the sqrtPriceLimitX96 based on the swap direction
if (zeroForOne) {
// Swapping token0 for token1
// sqrtPriceLimitX96 must be less than current price but greater than MIN_SQRT_RATIO
limit = TickMath.MIN_SQRT_RATIO + 1;
} else {
// Swapping token1 for token0
// sqrtPriceLimitX96 must be greater than current price but less than MAX_SQRT_RATIO
limit = TickMath.MAX_SQRT_RATIO - 1;
}
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 {
require(amount0Delta > 0 || amount1Delta > 0);
(address seller, , bool isBuy) = abi.decode(_data, (address, uint256, bool));
(, uint256 amountToPay) = amount0Delta > 0
? (!token0isWeth, uint256(amount0Delta))
: (token0isWeth, uint256(amount1Delta));
if (isBuy) {
weth.transfer(msg.sender, amountToPay);
} else {
require(harberg.transferFrom(seller, msg.sender, amountToPay), "Transfer failed");
}
}
}

View file

@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import {Harberg} from "../../src/Harberg.sol";
import {Stake} from "../../src/Stake.sol";
import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol";
contract MockSentimenter is Initializable, UUPSUpgradeable {
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);
}
/**
* @dev Throws if called by any account other than the admin.
*/
modifier onlyAdmin() {
_checkAdmin();
_;
}
/**
* @dev Throws if the sender is not the admin.
*/
function _checkAdmin() internal view virtual {
if (_getAdmin() != msg.sender) {
revert UnauthorizedAccount(msg.sender);
}
}
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {}
function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked) 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 returns (uint256 sentiment) {
uint256 percentageStaked = stake.getPercentageStaked();
uint256 averageTaxRate = stake.getAverageTaxRate();
sentiment = calculateSentiment(averageTaxRate, percentageStaked);
}
}