wip
This commit is contained in:
parent
ed2f71b9fd
commit
e62fb45ed5
7 changed files with 191 additions and 178 deletions
|
|
@ -1,3 +1,3 @@
|
||||||
@openzeppelin/=lib/openzeppelin-contracts/contracts/
|
@openzeppelin/=lib/openzeppelin-contracts/contracts/
|
||||||
@uniswap-v3-core/=lib/v3-core/
|
@uniswap-v3-core/=lib/uni-v3-lib/node_modules/@uniswap/v3-core/contracts/
|
||||||
@aperture/uni-v3-lib/=lib/uni-v3-lib/src/
|
@aperture/uni-v3-lib/=lib/uni-v3-lib/src/
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ contract GoerliScript is Script {
|
||||||
uint256 privateKey = vm.deriveKey(seedPhrase, 0);
|
uint256 privateKey = vm.deriveKey(seedPhrase, 0);
|
||||||
vm.startBroadcast(privateKey);
|
vm.startBroadcast(privateKey);
|
||||||
|
|
||||||
Blood bloodX = new BloodX("bloodX", "bXXX");
|
Harb harb = new Harb("Harberger Tax", "HARB");
|
||||||
Stake stakeX = new StakeX(address(bloodX));
|
Stake stake = new Stake(address(harb));
|
||||||
blood.setStakingContract(address(stakeX));
|
harb.setStakingPool(address(stake));
|
||||||
|
|
||||||
vm.stopBroadcast();
|
vm.stopBroadcast();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ pragma solidity ^0.8.19;
|
||||||
import { ERC20, IERC20, IERC20Metadata } from "@openzeppelin/token/ERC20/ERC20.sol";
|
import { ERC20, IERC20, IERC20Metadata } from "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
import { ERC20Permit, IERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
|
import { ERC20Permit, IERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
|
||||||
import { SafeCast } from "@openzeppelin/utils/math/SafeCast.sol";
|
import { SafeCast } from "@openzeppelin/utils/math/SafeCast.sol";
|
||||||
|
import { IStake } from "./interfaces/IStake.sol";
|
||||||
import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
|
import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,7 +23,9 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
TwabController public immutable twabController;
|
TwabController public immutable twabController;
|
||||||
|
|
||||||
/// @notice Address of the LiquidityManager contract that mints and burns supply
|
/// @notice Address of the LiquidityManager contract that mints and burns supply
|
||||||
address public immutable liquidityManager;
|
address public liquidityManager;
|
||||||
|
|
||||||
|
address public stakingPool;
|
||||||
|
|
||||||
/* ============ Errors ============ */
|
/* ============ Errors ============ */
|
||||||
|
|
||||||
|
|
@ -46,39 +48,38 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
constructor(
|
constructor(
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
TwabController twabController_,
|
TwabController twabController_
|
||||||
address liquidityManager_
|
|
||||||
) ERC20(name_, symbol_) ERC20Permit(name_) {
|
) ERC20(name_, symbol_) ERC20Permit(name_) {
|
||||||
if (address(0) == address(twabController_)) revert ZeroAddressInConstructor();
|
if (address(0) == address(twabController_)) revert ZeroAddressInConstructor();
|
||||||
twabController = twabController_;
|
twabController = twabController_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLiquidityManager(address liquidityManager_) external {
|
||||||
|
// TODO: add trapdoor
|
||||||
if (address(0) == liquidityManager_) revert ZeroAddressInConstructor();
|
if (address(0) == liquidityManager_) revert ZeroAddressInConstructor();
|
||||||
liquidityManager = liquidityManager_;
|
liquidityManager = liquidityManager_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setStakingPool(address stakingPool_) external {
|
||||||
|
// TODO: add trapdoor
|
||||||
|
if (address(0) == stakingPool_) revert ZeroAddressInConstructor();
|
||||||
|
stakingPool = stakingPool_;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============ External Functions ============ */
|
/* ============ External Functions ============ */
|
||||||
|
|
||||||
/// @notice Allows the liquidityManager to mint tokens for itself
|
/// @notice Allows the liquidityManager to mint tokens for itself
|
||||||
/// @dev May be overridden to provide more granular control over minting
|
/// @dev May be overridden to provide more granular control over minting
|
||||||
/// @param _amount Amount of tokens to mint
|
/// @param _amount Amount of tokens to mint
|
||||||
function mint(uint256 _amount)
|
function mint(uint256 _amount) external onlyLiquidityManager {
|
||||||
external
|
_mintHarb(liquidityManager, _amount);
|
||||||
virtual
|
|
||||||
override
|
|
||||||
onlyLiquidityManager
|
|
||||||
{
|
|
||||||
_mint(liquidityManager, _amount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Allows the liquidityManager to burn tokens from a its account
|
/// @notice Allows the liquidityManager to burn tokens from a its account
|
||||||
/// @dev May be overridden to provide more granular control over burning
|
/// @dev May be overridden to provide more granular control over burning
|
||||||
/// @param _amount Amount of tokens to burn
|
/// @param _amount Amount of tokens to burn
|
||||||
function burn(uint256 _amount)
|
function burn(uint256 _amount) external onlyLiquidityManager {
|
||||||
external
|
_burnHarb(liquidityManager, _amount);
|
||||||
virtual
|
|
||||||
override
|
|
||||||
onlyLiquidityManager
|
|
||||||
{
|
|
||||||
_burn(liquidityManager, _amount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============ Public ERC20 Overrides ============ */
|
/* ============ Public ERC20 Overrides ============ */
|
||||||
|
|
@ -102,21 +103,22 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
/**
|
/**
|
||||||
* @notice Mints tokens to `_receiver` and increases the total supply.
|
* @notice Mints tokens to `_receiver` and increases the total supply.
|
||||||
* @dev Emits a {Transfer} event with `from` set to the zero address.
|
* @dev Emits a {Transfer} event with `from` set to the zero address.
|
||||||
* @dev `_receiver` cannot be the zero address.
|
* @dev `receiver` cannot be the zero address.
|
||||||
* @param _receiver Address that will receive the minted tokens
|
* @param receiver Address that will receive the minted tokens
|
||||||
* @param _amount Tokens to mint
|
* @param amount Tokens to mint
|
||||||
*/
|
*/
|
||||||
function _mint(address _receiver, uint256 _amount) internal virtual override {
|
function _mintHarb(address receiver, uint256 amount) internal {
|
||||||
// make sure staking pool grows proportional to economy
|
// make sure staking pool grows proportional to economy
|
||||||
uint256 stakingPoolBalance = stakingPool();
|
uint256 stakingPoolBalance = balanceOf(stakingPool);
|
||||||
uint256 dormantStake = IStakeX(stakingContract).dormantSupply();
|
uint256 activeSupply = totalSupply - stakingPoolBalance;
|
||||||
|
uint256 dormantStake = IStake(stakingPool).dormantSupply();
|
||||||
if (stakingPoolBalance > 0) {
|
if (stakingPoolBalance > 0) {
|
||||||
uint256 newStake = stakingPoolBalance * amount / (activeSupply() + dormantStake);
|
uint256 newStake = stakingPoolBalance * amount / (activeSupply + dormantStake);
|
||||||
_mint(stakingContract, newStake);
|
_mint(stakingPool, newStake);
|
||||||
}
|
}
|
||||||
|
|
||||||
twabController.mint(_receiver, SafeCast.toUint96(_amount));
|
twabController.mint(receiver, SafeCast.toUint96(amount));
|
||||||
emit Transfer(address(0), _receiver, _amount);
|
emit Transfer(address(0), receiver, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -127,7 +129,7 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
* @param _owner The owner of the tokens
|
* @param _owner The owner of the tokens
|
||||||
* @param _amount The amount of tokens to burn
|
* @param _amount The amount of tokens to burn
|
||||||
*/
|
*/
|
||||||
function _burn(address _owner, uint256 _amount) internal virtual override {
|
function _burnHarb(address _owner, uint256 _amount) internal {
|
||||||
// TODO
|
// TODO
|
||||||
twabController.burn(_owner, SafeCast.toUint96(_amount));
|
twabController.burn(_owner, SafeCast.toUint96(_amount));
|
||||||
emit Transfer(_owner, address(0), _amount);
|
emit Transfer(_owner, address(0), _amount);
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@ pragma solidity ^0.8.20;
|
||||||
|
|
||||||
import './lib/PositionKey.sol';
|
import './lib/PositionKey.sol';
|
||||||
import './lib/FixedPoint128.sol';
|
import './lib/FixedPoint128.sol';
|
||||||
import './lib/FixedPoint96.sol';
|
|
||||||
import '@uniswap-v3-core/contracts/interfaces/IUniswapV3Pool.sol';
|
|
||||||
import '@aperture/uni-v3-lib/TickMath.sol';
|
import '@aperture/uni-v3-lib/TickMath.sol';
|
||||||
import '@aperture/uni-v3-lib/LiquidityAmounts.sol';
|
import '@aperture/uni-v3-lib/LiquidityAmounts.sol';
|
||||||
import '@aperture/uni-v3-lib/PoolAddress.sol';
|
import '@aperture/uni-v3-lib/PoolAddress.sol';
|
||||||
import '@aperture/uni-v3-lib/CallbackValidation.sol';
|
import '@aperture/uni-v3-lib/CallbackValidation.sol';
|
||||||
|
import '@uniswap-v3-core/interfaces/IUniswapV3Pool.sol';
|
||||||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -259,23 +258,23 @@ contract LiquidityManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function compareTokenToEthBalance(uint256 ethAmountInPosition, uint256 tokenAmountInPosition) external view returns (bool hasMoreToken) {
|
// function compareTokenToEthBalance(uint256 ethAmountInPosition, uint256 tokenAmountInPosition) external view returns (bool hasMoreToken) {
|
||||||
// Fetch the current sqrtPriceX96 from the pool
|
// // Fetch the current sqrtPriceX96 from the pool
|
||||||
(uint160 sqrtPriceX96,,,) = uniswapV3Pool.slot0();
|
// (uint160 sqrtPriceX96,,,) = uniswapV3Pool.slot0();
|
||||||
|
|
||||||
// Convert sqrtPriceX96 to a conventional price format
|
// // Convert sqrtPriceX96 to a conventional price format
|
||||||
// Note: The price is calculated as (sqrtPriceX96^2 / 2^192), simplified here as (price / 2^96) for the sake of example
|
// // Note: The price is calculated as (sqrtPriceX96^2 / 2^192), simplified here as (price / 2^96) for the sake of example
|
||||||
uint256 price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) / (1 << 96);
|
// uint256 price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) / (1 << 96);
|
||||||
|
|
||||||
// Calculate the equivalent token amount for the ETH in the position at the current price
|
// // Calculate the equivalent token amount for the ETH in the position at the current price
|
||||||
// Assuming price is expressed as the amount of token per ETH
|
// // Assuming price is expressed as the amount of token per ETH
|
||||||
uint256 equivalentTokenAmountForEth = ethAmountInPosition * price;
|
// uint256 equivalentTokenAmountForEth = ethAmountInPosition * price;
|
||||||
|
|
||||||
// Compare to the actual token amount in the position
|
// // Compare to the actual token amount in the position
|
||||||
hasMoreToken = tokenAmountInPosition > equivalentTokenAmountForEth;
|
// hasMoreToken = tokenAmountInPosition > equivalentTokenAmountForEth;
|
||||||
|
|
||||||
return hasMoreToken;
|
// return hasMoreToken;
|
||||||
}
|
// }
|
||||||
|
|
||||||
////////
|
////////
|
||||||
// - check if tick in range, otherwise revert
|
// - check if tick in range, otherwise revert
|
||||||
|
|
@ -289,56 +288,56 @@ contract LiquidityManager {
|
||||||
// - withdraw
|
// - withdraw
|
||||||
// - burn tokens
|
// - burn tokens
|
||||||
|
|
||||||
function rebalance(address token, int24 tickLower, int24 tickUpper) external {
|
// function rebalance(address token, int24 tickLower, int24 tickUpper) external {
|
||||||
bool ETH_TOKEN_ZERO = _weth < token;
|
// bool ETH_TOKEN_ZERO = WETH9 < token;
|
||||||
|
|
||||||
PoolKey memory poolKey = PoolAddress.getPoolKey(params.token0, params.token1, FEE);
|
// PoolKey memory poolKey = PoolAddress.getPoolKey(params.token0, params.token1, FEE);
|
||||||
IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
|
// IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
|
||||||
|
|
||||||
// Fetch the current tick from the Uniswap V3 pool
|
// // Fetch the current tick from the Uniswap V3 pool
|
||||||
(, int24 currentTick, , , , , ) = pool.slot0();
|
// (, int24 currentTick, , , , , ) = pool.slot0();
|
||||||
|
|
||||||
// Check if current tick is within the specified range
|
// // Check if current tick is within the specified range
|
||||||
require(currentTick >= tickLower && currentTick <= tickUpper, "Current tick out of range");
|
// require(currentTick >= tickLower && currentTick <= tickUpper, "Current tick out of range");
|
||||||
|
|
||||||
// load position
|
// // load position
|
||||||
TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)];
|
// TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)];
|
||||||
|
|
||||||
// take the position out
|
// // take the position out
|
||||||
uint256 (amount0, amount1) = pool.burn(tickLower, tickUpper, position.liquidity);
|
// uint256 (amount0, amount1) = pool.burn(tickLower, tickUpper, position.liquidity);
|
||||||
// TODO: this position might have earned fees, update them here
|
// // TODO: this position might have earned fees, update them here
|
||||||
|
|
||||||
// calculate liquidity
|
// // calculate liquidity
|
||||||
uint128 liquidity;
|
// uint128 liquidity;
|
||||||
if (ETH_TOKEN_ZERO) {
|
// if (ETH_TOKEN_ZERO) {
|
||||||
// extend/contract the range up
|
// // extend/contract the range up
|
||||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
|
// uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
|
||||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + (currentTick - tickLower));
|
// uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + (currentTick - tickLower));
|
||||||
liquidity = LiquidityAmounts.getLiquidityForAmount0(
|
// liquidity = LiquidityAmounts.getLiquidityForAmount0(
|
||||||
sqrtRatioAX96, sqrtRatioBX96, amount0
|
// sqrtRatioAX96, sqrtRatioBX96, amount0
|
||||||
)
|
// );
|
||||||
// calculate amount for new liquidity
|
// // calculate amount for new liquidity
|
||||||
uint256 newAmount1 = LiquidityAmounts.getAmount1ForLiquidity(
|
// uint256 newAmount1 = LiquidityAmounts.getAmount1ForLiquidity(
|
||||||
sqrtRatioAX96, sqrtRatioBX96, liquidity
|
// sqrtRatioAX96, sqrtRatioBX96, liquidity
|
||||||
)
|
// );
|
||||||
if (newAmount1 > amount1) {
|
// if (newAmount1 > amount1) {
|
||||||
IERC20(token).mint(address(this), newAmount1 - amount1 + 1);
|
// IERC20(token).mint(address(this), newAmount1 - amount1 + 1);
|
||||||
} else {
|
// } else {
|
||||||
IERC20(token).burn(address(this), amount1 - newAmount1 + 1);
|
// IERC20(token).burn(address(this), amount1 - newAmount1 + 1);
|
||||||
}
|
// }
|
||||||
|
|
||||||
} else {
|
// } else {
|
||||||
// extend/contract the range down
|
// // extend/contract the range down
|
||||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickUpper);
|
// uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickUpper);
|
||||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - (tickUpper - currentTick));
|
// uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - (tickUpper - currentTick));
|
||||||
liquidity = LiquidityAmounts.getLiquidityForAmount1(
|
// liquidity = LiquidityAmounts.getLiquidityForAmount1(
|
||||||
sqrtRatioAX96, sqrtRatioBX96, ethAmountToProvide
|
// sqrtRatioAX96, sqrtRatioBX96, ethAmountToProvide
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
pragma solidity ^0.8.13;
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
|
import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
|
||||||
import "./interfaces/IStake.sol";
|
import "./interfaces/IStake.sol";
|
||||||
import "./interfaces/IHarb.sol";
|
import "./interfaces/IHarb.sol";
|
||||||
|
|
||||||
|
|
@ -18,7 +20,7 @@ contract Stake is IStake {
|
||||||
error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId);
|
error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId);
|
||||||
error SharesTooLow(address receiver, uint256 assets, uint256 sharesWanted, uint256 minStake);
|
error SharesTooLow(address receiver, uint256 assets, uint256 sharesWanted, uint256 minStake);
|
||||||
error NoPermission(address requester, address owner);
|
error NoPermission(address requester, address owner);
|
||||||
error PossitionNotFound
|
error PositionNotFound();
|
||||||
|
|
||||||
|
|
||||||
struct StakingPosition {
|
struct StakingPosition {
|
||||||
|
|
@ -51,15 +53,27 @@ contract Stake is IStake {
|
||||||
return totalSupply * (100 - MAX_STAKE) / 100;
|
return totalSupply * (100 - MAX_STAKE) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function authorizedStake() private pure returns(uint256) {
|
||||||
|
return totalSupply * MAX_STAKE / 100;
|
||||||
|
}
|
||||||
|
|
||||||
function assetsToShares(uint256 assets) private view returns (uint256) {
|
function assetsToShares(uint256 assets) private view returns (uint256) {
|
||||||
return assets * totalSupply / IERC20(_tokenContract).totalSupply();
|
return assets * totalSupply / IERC20(tokenContract).totalSupply();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sharesToAssets(uint256 shares) private view returns (uint256) {
|
function sharesToAssets(uint256 shares) private view returns (uint256) {
|
||||||
return shares * IERC20(_tokenContract).totalSupply() / totalSupply;
|
return shares * IERC20(tokenContract).totalSupply() / totalSupply;
|
||||||
}
|
}
|
||||||
|
|
||||||
function snatch(uint256 assets, address receiver, uint64 taxRate, uint256[] positions) public returns(uint256) {
|
/**
|
||||||
|
|
||||||
|
* TODO: deal with metatransactions: While these are generally available
|
||||||
|
* via msg.sender and msg.data, they should not be accessed in such a direct
|
||||||
|
* manner, since when dealing with meta-transactions the account sending and
|
||||||
|
* paying for execution may not be the actual sender (as far as an application
|
||||||
|
* is concerned).
|
||||||
|
*/
|
||||||
|
function snatch(uint256 assets, address receiver, uint64 taxRate, uint256[] calldata positionsToSnatch) public returns(uint256) {
|
||||||
|
|
||||||
// check lower boundary
|
// check lower boundary
|
||||||
uint256 sharesWanted = assetsToShares(assets);
|
uint256 sharesWanted = assetsToShares(assets);
|
||||||
|
|
@ -67,12 +81,12 @@ contract Stake is IStake {
|
||||||
revert SharesTooLow(receiver, assets, sharesWanted, minStake);
|
revert SharesTooLow(receiver, assets, sharesWanted, minStake);
|
||||||
}
|
}
|
||||||
|
|
||||||
// run through all suggested positions
|
// run through all suggested positions to snatch
|
||||||
for (uint i = 0; i < positions.length; i++) {
|
for (uint i = 0; i < positionsToSnatch.length; i++) {
|
||||||
StakingPosition pos = positions[i];
|
StakingPosition storage pos = positions[positionsToSnatch[i]];
|
||||||
if (pos.creationTime == 0) {
|
if (pos.creationTime == 0) {
|
||||||
//TODO:
|
//TODO:
|
||||||
revert PossitionNotFound();
|
revert PositionNotFound();
|
||||||
}
|
}
|
||||||
// check that tax lower
|
// check that tax lower
|
||||||
if (taxRate <= pos.perSecondTaxRate) {
|
if (taxRate <= pos.perSecondTaxRate) {
|
||||||
|
|
@ -84,17 +98,17 @@ contract Stake is IStake {
|
||||||
}
|
}
|
||||||
|
|
||||||
// now try to make a new position in the free space and hope it is big enough
|
// now try to make a new position in the free space and hope it is big enough
|
||||||
uint256 availableStake = authorizedStake - outstandingStake;
|
uint256 availableStake = authorizedStake() - outstandingStake;
|
||||||
if (sharesWanted > availableStake) {
|
if (sharesWanted > availableStake) {
|
||||||
revert ExceededAvailableStake(receiver, sharesWanted, availableStake);
|
revert ExceededAvailableStake(receiver, sharesWanted, availableStake);
|
||||||
}
|
}
|
||||||
|
|
||||||
// transfer
|
// transfer
|
||||||
SafeERC20.safeTransferFrom(tokenContract, _msgSender(), address(this), assets);
|
SafeERC20.safeTransferFrom(tokenContract, msg.sender, address(this), assets);
|
||||||
|
|
||||||
// mint
|
// mint
|
||||||
StakingPosition storage sp = c.funders[lastTokenId++];
|
StakingPosition storage sp = positions[lastTokenId++];
|
||||||
sp.share = shares;
|
sp.share = sharesWanted;
|
||||||
sp.owner = receiver;
|
sp.owner = receiver;
|
||||||
sp.lastTaxTime = now;
|
sp.lastTaxTime = now;
|
||||||
sp.creationTime = now;
|
sp.creationTime = now;
|
||||||
|
|
@ -107,21 +121,18 @@ contract Stake is IStake {
|
||||||
|
|
||||||
|
|
||||||
function exitPosition(uint256 positionID) public {
|
function exitPosition(uint256 positionID) public {
|
||||||
StakingPosition pos = positions[positionID];
|
StakingPosition storage pos = positions[positionID];
|
||||||
if(pos.owner != _msgSender()) {
|
if(pos.owner != msg.sender) {
|
||||||
NoPermission(_msgSender(), pos.owner);
|
NoPermission(msg.sender, pos.owner);
|
||||||
}
|
|
||||||
// to prevent snatch-and-exit grieving attack
|
|
||||||
if(now - pos.creationTime < 60 * 60 * 24 * 3) {
|
|
||||||
|
|
||||||
ExitTooEarly(pos.owner, positionID, pos.creationTime);
|
|
||||||
}
|
}
|
||||||
|
// to prevent snatch-and-exit grieving attack, pay TAX_FLOOR_DURATION
|
||||||
_payTax(pos, TAX_FLOOR_DURATION);
|
_payTax(pos, TAX_FLOOR_DURATION);
|
||||||
_exitPosition(pos);
|
_exitPosition(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: what if someone calls payTax and exitPosition in the same transaction?
|
||||||
function payTax(uint256 positionID) public {
|
function payTax(uint256 positionID) public {
|
||||||
StakingPosition pos = positions[positionID];
|
StakingPosition storage pos = positions[positionID];
|
||||||
_payTax(pos, 0);
|
_payTax(pos, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,12 +150,13 @@ contract Stake is IStake {
|
||||||
SafeERC20.safeTransfer(tokenContract, taxPool, taxDue);
|
SafeERC20.safeTransfer(tokenContract, taxPool, taxDue);
|
||||||
if (assetsBefore - taxDue > 0) {
|
if (assetsBefore - taxDue > 0) {
|
||||||
// if something left over, update storage
|
// if something left over, update storage
|
||||||
sp.shares = assetsToShares(assetsBefore - taxDue);
|
pos.shares = assetsToShares(assetsBefore - taxDue);
|
||||||
sp.lastTaxTime = now;
|
pos.lastTaxTime = now;
|
||||||
} else {
|
} else {
|
||||||
// if nothing left over, liquidate position
|
// if nothing left over, liquidate position
|
||||||
outstandingStake -= sp.share;
|
// TODO: emit event
|
||||||
delete sp;
|
outstandingStake -= pos.share;
|
||||||
|
delete pos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
pragma solidity ^0.8.13;
|
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
|
||||||
import "forge-std/console.sol";
|
|
||||||
import "../src/BloodX.sol";
|
|
||||||
import "../src/StakeX.sol";
|
|
||||||
|
|
||||||
contract BloodXTest is Test {
|
|
||||||
BloodX public bloodX;
|
|
||||||
StakeX public stakeX;
|
|
||||||
uint256 constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
|
|
||||||
|
|
||||||
function setUp() public {
|
|
||||||
bloodX = new BloodX("name", "SYM");
|
|
||||||
stakeX = new StakeX("nameStake", "SYS", address(bloodX));
|
|
||||||
bloodX.setStakingContract(address(stakeX));
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_MintStakeUnstake(address account, uint256 amount) public {
|
|
||||||
vm.assume(amount > 1);
|
|
||||||
vm.assume(amount < MAX_INT / 100000 ether);
|
|
||||||
vm.assume(account != address(0));
|
|
||||||
vm.assume(account != address(bloodX));
|
|
||||||
vm.assume(account != address(stakeX));
|
|
||||||
|
|
||||||
// test mint
|
|
||||||
uint256 totalSupplyBefore = bloodX.totalSupply();
|
|
||||||
uint256 balanceBefore = bloodX.balanceOf(account);
|
|
||||||
bloodX.purchase(account, amount);
|
|
||||||
uint256 totalAfter = bloodX.totalSupply();
|
|
||||||
assertEq(totalAfter, totalSupplyBefore + amount, "total supply should match");
|
|
||||||
assertEq(bloodX.balanceOf(account), balanceBefore + amount, "balance should match");
|
|
||||||
|
|
||||||
// test stake
|
|
||||||
uint256 newStake = amount / 2 * 100000 ether / totalAfter;
|
|
||||||
{
|
|
||||||
uint256 outstandingBefore = stakeX.totalSupply();
|
|
||||||
uint256 stakeBalanceBefore = stakeX.balanceOf(account);
|
|
||||||
vm.prank(account);
|
|
||||||
bloodX.stake(account, amount / 2);
|
|
||||||
assertEq(bloodX.totalSupply(), totalSupplyBefore + (amount - (amount / 2)), "total supply should match after stake");
|
|
||||||
assertEq(bloodX.balanceOf(account), balanceBefore + (amount - (amount / 2)), "balance should match after stake");
|
|
||||||
assertEq(outstandingBefore + newStake, stakeX.totalSupply(), "outstanding supply should match");
|
|
||||||
assertEq(stakeBalanceBefore + newStake, stakeX.balanceOf(account), "balance of stake account should match");
|
|
||||||
}
|
|
||||||
|
|
||||||
// test unstake
|
|
||||||
{
|
|
||||||
(uint256 totalBefore, uint256 leftBefore,) = stakeX.getUnstakeSlot(account);
|
|
||||||
vm.prank(account);
|
|
||||||
stakeX.unstake(account, newStake / 2);
|
|
||||||
uint256 timeBefore = block.timestamp;
|
|
||||||
vm.warp(timeBefore + 60 * 60 * 36);
|
|
||||||
stakeX.unstakeTick(account);
|
|
||||||
(uint256 total, uint256 left, uint256 start) = stakeX.getUnstakeSlot(account);
|
|
||||||
assertEq(total, totalBefore + (newStake / 2), "total unstake should match");
|
|
||||||
assertApproxEqAbs(left, leftBefore + (newStake / 4), 1);
|
|
||||||
assertEq(start, timeBefore, "time unstake should match");
|
|
||||||
vm.warp(timeBefore + 60 * 60 * 72);
|
|
||||||
stakeX.unstakeTick(account);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
65
onchain/test/Harb.t.sol
Normal file
65
onchain/test/Harb.t.sol
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
import "forge-std/console.sol";
|
||||||
|
import "../src/Harb.sol";
|
||||||
|
import "../src/Stake.sol";
|
||||||
|
|
||||||
|
contract BloodXTest is Test {
|
||||||
|
Harb public harb;
|
||||||
|
Stake public stake;
|
||||||
|
uint256 constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
harb = new Harb("name", "SYM");
|
||||||
|
stake = new Stake(address(harb));
|
||||||
|
harb.setStakingPool(address(stake));
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_MintStakeUnstake(address account, uint256 amount) public {
|
||||||
|
vm.assume(amount > 1);
|
||||||
|
vm.assume(amount < MAX_INT / 100000 ether);
|
||||||
|
vm.assume(account != address(0));
|
||||||
|
vm.assume(account != address(harb));
|
||||||
|
vm.assume(account != address(stake));
|
||||||
|
|
||||||
|
// test mint
|
||||||
|
uint256 totalSupplyBefore = harb.totalSupply();
|
||||||
|
uint256 balanceBefore = harb.balanceOf(account);
|
||||||
|
harb.purchase(account, amount);
|
||||||
|
uint256 totalAfter = harb.totalSupply();
|
||||||
|
assertEq(totalAfter, totalSupplyBefore + amount, "total supply should match");
|
||||||
|
assertEq(harb.balanceOf(account), balanceBefore + amount, "balance should match");
|
||||||
|
|
||||||
|
// test stake
|
||||||
|
uint256 newStake = amount / 2 * 100000 ether / totalAfter;
|
||||||
|
{
|
||||||
|
uint256 outstandingBefore = stake.totalSupply();
|
||||||
|
uint256 stakeBalanceBefore = stake.balanceOf(account);
|
||||||
|
vm.prank(account);
|
||||||
|
harb.stake(account, amount / 2);
|
||||||
|
assertEq(harb.totalSupply(), totalSupplyBefore + (amount - (amount / 2)), "total supply should match after stake");
|
||||||
|
assertEq(harb.balanceOf(account), balanceBefore + (amount - (amount / 2)), "balance should match after stake");
|
||||||
|
assertEq(outstandingBefore + newStake, stake.totalSupply(), "outstanding supply should match");
|
||||||
|
assertEq(stakeBalanceBefore + newStake, stake.balanceOf(account), "balance of stake account should match");
|
||||||
|
}
|
||||||
|
|
||||||
|
// test unstake
|
||||||
|
{
|
||||||
|
(uint256 totalBefore, uint256 leftBefore,) = stake.getUnstakeSlot(account);
|
||||||
|
vm.prank(account);
|
||||||
|
stake.unstake(account, newStake / 2);
|
||||||
|
uint256 timeBefore = block.timestamp;
|
||||||
|
vm.warp(timeBefore + 60 * 60 * 36);
|
||||||
|
stake.unstakeTick(account);
|
||||||
|
(uint256 total, uint256 left, uint256 start) = stake.getUnstakeSlot(account);
|
||||||
|
assertEq(total, totalBefore + (newStake / 2), "total unstake should match");
|
||||||
|
assertApproxEqAbs(left, leftBefore + (newStake / 4), 1);
|
||||||
|
assertEq(start, timeBefore, "time unstake should match");
|
||||||
|
vm.warp(timeBefore + 60 * 60 * 72);
|
||||||
|
stake.unstakeTick(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue