diff --git a/onchain/remappings.txt b/onchain/remappings.txt index 5c00446..5ea0f2a 100644 --- a/onchain/remappings.txt +++ b/onchain/remappings.txt @@ -1,3 +1,3 @@ @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/ diff --git a/onchain/script/Deploy.sol b/onchain/script/Deploy.sol index 5be4cc0..6292fc8 100644 --- a/onchain/script/Deploy.sol +++ b/onchain/script/Deploy.sol @@ -12,9 +12,9 @@ contract GoerliScript is Script { uint256 privateKey = vm.deriveKey(seedPhrase, 0); vm.startBroadcast(privateKey); - Blood bloodX = new BloodX("bloodX", "bXXX"); - Stake stakeX = new StakeX(address(bloodX)); - blood.setStakingContract(address(stakeX)); + Harb harb = new Harb("Harberger Tax", "HARB"); + Stake stake = new Stake(address(harb)); + harb.setStakingPool(address(stake)); vm.stopBroadcast(); } diff --git a/onchain/src/Harb.sol b/onchain/src/Harb.sol index dd04968..e9849bf 100644 --- a/onchain/src/Harb.sol +++ b/onchain/src/Harb.sol @@ -5,7 +5,7 @@ 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 { SafeCast } from "@openzeppelin/utils/math/SafeCast.sol"; - +import { IStake } from "./interfaces/IStake.sol"; import { TwabController } from "pt-v5-twab-controller/TwabController.sol"; /** @@ -23,7 +23,9 @@ contract Harb is ERC20, ERC20Permit { TwabController public immutable twabController; /// @notice Address of the LiquidityManager contract that mints and burns supply - address public immutable liquidityManager; + address public liquidityManager; + + address public stakingPool; /* ============ Errors ============ */ @@ -46,39 +48,38 @@ contract Harb is ERC20, ERC20Permit { constructor( string memory name_, string memory symbol_, - TwabController twabController_, - address liquidityManager_ + TwabController twabController_ ) ERC20(name_, symbol_) ERC20Permit(name_) { if (address(0) == address(twabController_)) revert ZeroAddressInConstructor(); twabController = twabController_; + } + + function setLiquidityManager(address liquidityManager_) external { + // TODO: add trapdoor if (address(0) == liquidityManager_) revert ZeroAddressInConstructor(); liquidityManager = liquidityManager_; } + function setStakingPool(address stakingPool_) external { + // TODO: add trapdoor + if (address(0) == stakingPool_) revert ZeroAddressInConstructor(); + stakingPool = stakingPool_; + } + /* ============ External Functions ============ */ /// @notice Allows the liquidityManager to mint tokens for itself /// @dev May be overridden to provide more granular control over minting /// @param _amount Amount of tokens to mint - function mint(uint256 _amount) - external - virtual - override - onlyLiquidityManager - { - _mint(liquidityManager, _amount); + function mint(uint256 _amount) external onlyLiquidityManager { + _mintHarb(liquidityManager, _amount); } /// @notice Allows the liquidityManager to burn tokens from a its account /// @dev May be overridden to provide more granular control over burning /// @param _amount Amount of tokens to burn - function burn(uint256 _amount) - external - virtual - override - onlyLiquidityManager - { - _burn(liquidityManager, _amount); + function burn(uint256 _amount) external onlyLiquidityManager { + _burnHarb(liquidityManager, _amount); } /* ============ Public ERC20 Overrides ============ */ @@ -102,21 +103,22 @@ contract Harb is ERC20, ERC20Permit { /** * @notice Mints tokens to `_receiver` and increases the total supply. * @dev Emits a {Transfer} event with `from` set to the zero address. - * @dev `_receiver` cannot be the zero address. - * @param _receiver Address that will receive the minted tokens - * @param _amount Tokens to mint + * @dev `receiver` cannot be the zero address. + * @param receiver Address that will receive the minted tokens + * @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 - uint256 stakingPoolBalance = stakingPool(); - uint256 dormantStake = IStakeX(stakingContract).dormantSupply(); + uint256 stakingPoolBalance = balanceOf(stakingPool); + uint256 activeSupply = totalSupply - stakingPoolBalance; + uint256 dormantStake = IStake(stakingPool).dormantSupply(); if (stakingPoolBalance > 0) { - uint256 newStake = stakingPoolBalance * amount / (activeSupply() + dormantStake); - _mint(stakingContract, newStake); + uint256 newStake = stakingPoolBalance * amount / (activeSupply + dormantStake); + _mint(stakingPool, newStake); } - twabController.mint(_receiver, SafeCast.toUint96(_amount)); - emit Transfer(address(0), _receiver, _amount); + twabController.mint(receiver, SafeCast.toUint96(amount)); + emit Transfer(address(0), receiver, amount); } /** @@ -127,7 +129,7 @@ contract Harb is ERC20, ERC20Permit { * @param _owner The owner of the tokens * @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 twabController.burn(_owner, SafeCast.toUint96(_amount)); emit Transfer(_owner, address(0), _amount); diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 8a73e1d..919e52e 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -3,12 +3,11 @@ pragma solidity ^0.8.20; import './lib/PositionKey.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/LiquidityAmounts.sol'; import '@aperture/uni-v3-lib/PoolAddress.sol'; import '@aperture/uni-v3-lib/CallbackValidation.sol'; +import '@uniswap-v3-core/interfaces/IUniswapV3Pool.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) { - // Fetch the current sqrtPriceX96 from the pool - (uint160 sqrtPriceX96,,,) = uniswapV3Pool.slot0(); + // function compareTokenToEthBalance(uint256 ethAmountInPosition, uint256 tokenAmountInPosition) external view returns (bool hasMoreToken) { + // // Fetch the current sqrtPriceX96 from the pool + // (uint160 sqrtPriceX96,,,) = uniswapV3Pool.slot0(); - // 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 - uint256 price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) / (1 << 96); + // // 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 + // uint256 price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) / (1 << 96); - // 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 - uint256 equivalentTokenAmountForEth = ethAmountInPosition * 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 + // uint256 equivalentTokenAmountForEth = ethAmountInPosition * price; - // Compare to the actual token amount in the position - hasMoreToken = tokenAmountInPosition > equivalentTokenAmountForEth; + // // Compare to the actual token amount in the position + // hasMoreToken = tokenAmountInPosition > equivalentTokenAmountForEth; - return hasMoreToken; - } + // return hasMoreToken; + // } //////// // - check if tick in range, otherwise revert @@ -289,56 +288,56 @@ contract LiquidityManager { // - withdraw // - burn tokens - function rebalance(address token, int24 tickLower, int24 tickUpper) external { - bool ETH_TOKEN_ZERO = _weth < token; + // function rebalance(address token, int24 tickLower, int24 tickUpper) external { + // bool ETH_TOKEN_ZERO = WETH9 < token; - PoolKey memory poolKey = PoolAddress.getPoolKey(params.token0, params.token1, FEE); - IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); + // PoolKey memory poolKey = PoolAddress.getPoolKey(params.token0, params.token1, FEE); + // IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); - // Fetch the current tick from the Uniswap V3 pool - (, int24 currentTick, , , , , ) = pool.slot0(); + // // Fetch the current tick from the Uniswap V3 pool + // (, int24 currentTick, , , , , ) = pool.slot0(); - // Check if current tick is within the specified range - require(currentTick >= tickLower && currentTick <= tickUpper, "Current tick out of range"); + // // Check if current tick is within the specified range + // require(currentTick >= tickLower && currentTick <= tickUpper, "Current tick out of range"); - // load position - TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)]; + // // load position + // TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)]; - // take the position out - uint256 (amount0, amount1) = pool.burn(tickLower, tickUpper, position.liquidity); - // TODO: this position might have earned fees, update them here + // // take the position out + // uint256 (amount0, amount1) = pool.burn(tickLower, tickUpper, position.liquidity); + // // TODO: this position might have earned fees, update them here - // calculate liquidity - uint128 liquidity; - if (ETH_TOKEN_ZERO) { - // extend/contract the range up - uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); - uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + (currentTick - tickLower)); - liquidity = LiquidityAmounts.getLiquidityForAmount0( - sqrtRatioAX96, sqrtRatioBX96, amount0 - ) - // calculate amount for new liquidity - uint256 newAmount1 = LiquidityAmounts.getAmount1ForLiquidity( - sqrtRatioAX96, sqrtRatioBX96, liquidity - ) - if (newAmount1 > amount1) { - IERC20(token).mint(address(this), newAmount1 - amount1 + 1); - } else { - IERC20(token).burn(address(this), amount1 - newAmount1 + 1); - } + // // calculate liquidity + // uint128 liquidity; + // if (ETH_TOKEN_ZERO) { + // // extend/contract the range up + // uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); + // uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + (currentTick - tickLower)); + // liquidity = LiquidityAmounts.getLiquidityForAmount0( + // sqrtRatioAX96, sqrtRatioBX96, amount0 + // ); + // // calculate amount for new liquidity + // uint256 newAmount1 = LiquidityAmounts.getAmount1ForLiquidity( + // sqrtRatioAX96, sqrtRatioBX96, liquidity + // ); + // if (newAmount1 > amount1) { + // IERC20(token).mint(address(this), newAmount1 - amount1 + 1); + // } else { + // IERC20(token).burn(address(this), amount1 - newAmount1 + 1); + // } - } else { - // extend/contract the range down - uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickUpper); - uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - (tickUpper - currentTick)); - liquidity = LiquidityAmounts.getLiquidityForAmount1( - sqrtRatioAX96, sqrtRatioBX96, ethAmountToProvide - ); - } + // } else { + // // extend/contract the range down + // uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickUpper); + // uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - (tickUpper - currentTick)); + // liquidity = LiquidityAmounts.getLiquidityForAmount1( + // sqrtRatioAX96, sqrtRatioBX96, ethAmountToProvide + // ); + // } - } + // } } \ No newline at end of file diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index 516fcef..4b8b3b0 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -2,6 +2,8 @@ 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/IHarb.sol"; @@ -18,7 +20,7 @@ contract Stake is IStake { error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId); error SharesTooLow(address receiver, uint256 assets, uint256 sharesWanted, uint256 minStake); error NoPermission(address requester, address owner); - error PossitionNotFound + error PositionNotFound(); struct StakingPosition { @@ -51,15 +53,27 @@ contract Stake is IStake { 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) { - return assets * totalSupply / IERC20(_tokenContract).totalSupply(); + return assets * totalSupply / IERC20(tokenContract).totalSupply(); } 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 uint256 sharesWanted = assetsToShares(assets); @@ -67,12 +81,12 @@ contract Stake is IStake { revert SharesTooLow(receiver, assets, sharesWanted, minStake); } - // run through all suggested positions - for (uint i = 0; i < positions.length; i++) { - StakingPosition pos = positions[i]; + // run through all suggested positions to snatch + for (uint i = 0; i < positionsToSnatch.length; i++) { + StakingPosition storage pos = positions[positionsToSnatch[i]]; if (pos.creationTime == 0) { //TODO: - revert PossitionNotFound(); + revert PositionNotFound(); } // check that tax lower 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 - uint256 availableStake = authorizedStake - outstandingStake; + uint256 availableStake = authorizedStake() - outstandingStake; if (sharesWanted > availableStake) { revert ExceededAvailableStake(receiver, sharesWanted, availableStake); } // transfer - SafeERC20.safeTransferFrom(tokenContract, _msgSender(), address(this), assets); + SafeERC20.safeTransferFrom(tokenContract, msg.sender, address(this), assets); // mint - StakingPosition storage sp = c.funders[lastTokenId++]; - sp.share = shares; + StakingPosition storage sp = positions[lastTokenId++]; + sp.share = sharesWanted; sp.owner = receiver; sp.lastTaxTime = now; sp.creationTime = now; @@ -107,21 +121,18 @@ contract Stake is IStake { function exitPosition(uint256 positionID) public { - StakingPosition pos = positions[positionID]; - if(pos.owner != _msgSender()) { - NoPermission(_msgSender(), pos.owner); - } - // to prevent snatch-and-exit grieving attack - if(now - pos.creationTime < 60 * 60 * 24 * 3) { - - ExitTooEarly(pos.owner, positionID, pos.creationTime); + StakingPosition storage pos = positions[positionID]; + if(pos.owner != msg.sender) { + NoPermission(msg.sender, pos.owner); } + // to prevent snatch-and-exit grieving attack, pay TAX_FLOOR_DURATION _payTax(pos, TAX_FLOOR_DURATION); _exitPosition(pos); } + // TODO: what if someone calls payTax and exitPosition in the same transaction? function payTax(uint256 positionID) public { - StakingPosition pos = positions[positionID]; + StakingPosition storage pos = positions[positionID]; _payTax(pos, 0); } @@ -139,12 +150,13 @@ contract Stake is IStake { SafeERC20.safeTransfer(tokenContract, taxPool, taxDue); if (assetsBefore - taxDue > 0) { // if something left over, update storage - sp.shares = assetsToShares(assetsBefore - taxDue); - sp.lastTaxTime = now; + pos.shares = assetsToShares(assetsBefore - taxDue); + pos.lastTaxTime = now; } else { // if nothing left over, liquidate position - outstandingStake -= sp.share; - delete sp; + // TODO: emit event + outstandingStake -= pos.share; + delete pos; } } diff --git a/onchain/test/BloodX.t.sol b/onchain/test/BloodX.t.sol deleted file mode 100644 index aa356a0..0000000 --- a/onchain/test/BloodX.t.sol +++ /dev/null @@ -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); - } - } - -} diff --git a/onchain/test/Harb.t.sol b/onchain/test/Harb.t.sol new file mode 100644 index 0000000..bd709a5 --- /dev/null +++ b/onchain/test/Harb.t.sol @@ -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); + } + } + +}