staking test cleanup
This commit is contained in:
parent
2b817c9331
commit
36833cab7f
10 changed files with 175 additions and 404 deletions
|
|
@ -113,9 +113,20 @@ address: 0xeB64dD1b4c0D59c9c8Cb3d9EA0E319cD0d45825f
|
|||
- add this function: https://github.com/721labs/partial-common-ownership/blob/3e7713bc60b6bb2e103320036ec5aeaaaceb7d2b/contracts/token/modules/Taxation.sol#L260
|
||||
- address this issue: "Seems like an owner could always frontrun buy attempts by increasing the valuation by one wei."
|
||||
- rename TAX_FLOOR_DURATION to cooldown?
|
||||
- burn tokens should affect stake
|
||||
- limit discovery position growth to max_issuance / day
|
||||
|
||||
open features:
|
||||
- token minting limit / limit on discovery position growth
|
||||
- ERC721 & ERC4907, user/owner separation, influencer incentives
|
||||
- token contract not visible in uniswap sepolia
|
||||
- snatch collision
|
||||
- previousTotalSupply at beginning?
|
||||
- profit for staking position
|
||||
- tax paid for staking position
|
||||
- partially snatched
|
||||
- liquidation bot
|
||||
- shift/slide bot
|
||||
- ubi claim bot
|
||||
|
||||
## old lp
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ contract SepoliaScript is Script {
|
|||
|
||||
TwabController tc = TwabController(TWABC);
|
||||
//TwabController tc = new TwabController(60 * 60, uint32(block.timestamp));
|
||||
Harb harb = new Harb("Harberger Tax", "HARB", V3_FACTORY, WETH, tc);
|
||||
Harb harb = new Harb("Harberger Tax", "HARB", tc);
|
||||
Stake stake = new Stake(address(harb));
|
||||
harb.setStakingPool(address(stake));
|
||||
IUniswapV3Factory factory = IUniswapV3Factory(V3_FACTORY);
|
||||
factory.createPool(WETH, address(harb), FEE);
|
||||
address liquidityPool = factory.createPool(WETH, address(harb), FEE);
|
||||
harb.setLiquidityPool(liquidityPool);
|
||||
BaseLineLP liquidityManager = new BaseLineLP(V3_FACTORY, WETH, address(harb));
|
||||
harb.setLiquidityManager(address(liquidityManager));
|
||||
vm.stopBroadcast();
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ contract BaseLineLP {
|
|||
if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed);
|
||||
}
|
||||
|
||||
function liquidityPool() external view returns (address) {
|
||||
return address(pool);
|
||||
}
|
||||
|
||||
function setFeeDestination(address feeDestination_) external {
|
||||
// TODO: add trapdoor
|
||||
require(address(0) != feeDestination_, "zero addr");
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@ 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";
|
||||
import {Math} from "@openzeppelin/utils/math/Math.sol";
|
||||
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import "@aperture/uni-v3-lib/PoolAddress.sol";
|
||||
|
||||
/**
|
||||
* @title TWAB ERC20 Token
|
||||
|
|
@ -23,24 +20,20 @@ contract Harb is ERC20, ERC20Permit {
|
|||
|
||||
address public constant TAX_POOL = address(2);
|
||||
uint24 constant FEE = uint24(10_000);
|
||||
|
||||
/* ============ Public Variables ============ */
|
||||
|
||||
/// @notice Address of the TwabController used to keep track of balances.
|
||||
TwabController public immutable twabController;
|
||||
uint256 immutable PERIOD_OFFSET;
|
||||
uint256 immutable PERIOD_LENGTH;
|
||||
IUniswapV3Pool immutable pool;
|
||||
|
||||
/// @notice Address of the LiquidityManager contract that mints and burns supply
|
||||
address public liquidityManager;
|
||||
|
||||
address public stakingPool;
|
||||
|
||||
/* ============ Public Variables ============ */
|
||||
uint256 public sumTaxCollected;
|
||||
|
||||
uint256 public previousTotalSupply;
|
||||
|
||||
//periphery contracts
|
||||
TwabController private immutable twabController;
|
||||
address private liquidityManager;
|
||||
address private stakingPool;
|
||||
address private liquidityPool;
|
||||
|
||||
|
||||
struct UbiTitle {
|
||||
uint256 sumTaxCollected;
|
||||
uint256 time;
|
||||
|
|
@ -70,7 +63,7 @@ contract Harb is ERC20, ERC20Permit {
|
|||
* @param name_ The name of the token
|
||||
* @param symbol_ The token symbol
|
||||
*/
|
||||
constructor(string memory name_, string memory symbol_, address _factory, address _WETH9, TwabController twabController_)
|
||||
constructor(string memory name_, string memory symbol_, TwabController twabController_)
|
||||
ERC20(name_, symbol_)
|
||||
ERC20Permit(name_)
|
||||
{
|
||||
|
|
@ -78,10 +71,13 @@ contract Harb is ERC20, ERC20Permit {
|
|||
twabController = twabController_;
|
||||
PERIOD_OFFSET = twabController.PERIOD_OFFSET();
|
||||
PERIOD_LENGTH = twabController.PERIOD_LENGTH();
|
||||
PoolKey memory poolKey = PoolAddress.getPoolKey(_WETH9, address(this), FEE);
|
||||
pool = IUniswapV3Pool(PoolAddress.computeAddress(_factory, poolKey));
|
||||
}
|
||||
|
||||
function setLiquidityPool(address liquidityPool_) external {
|
||||
if (address(0) == liquidityPool_) revert ZeroAddressInSetter();
|
||||
if (liquidityPool != address(0)) revert AddressAlreadySet();
|
||||
liquidityPool = liquidityPool_;
|
||||
}
|
||||
|
||||
function setLiquidityManager(address liquidityManager_) external {
|
||||
if (address(0) == liquidityManager_) revert ZeroAddressInSetter();
|
||||
|
|
@ -95,6 +91,10 @@ contract Harb is ERC20, ERC20Permit {
|
|||
stakingPool = stakingPool_;
|
||||
}
|
||||
|
||||
function getPeripheryContracts() external view returns (address, address, address, address) {
|
||||
return (address(twabController), liquidityManager, stakingPool, liquidityPool);
|
||||
}
|
||||
|
||||
/* ============ External Functions ============ */
|
||||
|
||||
/// @notice Allows the liquidityManager to mint tokens for itself
|
||||
|
|
@ -211,7 +211,7 @@ contract Harb is ERC20, ERC20Permit {
|
|||
}
|
||||
uint256 accountTwab = twabController.getTwabBetween(address(this), _account, lastTaxClaimed, lastPeriodEndAt);
|
||||
uint256 stakeTwab = twabController.getTwabBetween(address(this), stakingPool, lastTaxClaimed, lastPeriodEndAt);
|
||||
uint256 poolTwab = twabController.getTwabBetween(address(this), address(pool), lastTaxClaimed, lastPeriodEndAt);
|
||||
uint256 poolTwab = twabController.getTwabBetween(address(this), liquidityPool, lastTaxClaimed, lastPeriodEndAt);
|
||||
uint256 taxTwab = twabController.getTwabBetween(address(this), TAX_POOL, lastTaxClaimed, lastPeriodEndAt);
|
||||
uint256 totalSupplyTwab = twabController.getTotalSupplyTwabBetween(address(this), lastTaxClaimed, lastPeriodEndAt);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,12 @@ import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
|
|||
import "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
|
||||
import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
|
||||
import {Math} from "@openzeppelin/utils/math/Math.sol";
|
||||
import "./interfaces/IStake.sol";
|
||||
import "./Harb.sol";
|
||||
|
||||
error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake);
|
||||
error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStake, uint256 smallestShare);
|
||||
|
||||
contract Stake is IStake {
|
||||
contract Stake {
|
||||
using Math for uint256;
|
||||
|
||||
uint256 internal DECIMAL_OFFSET = 5 + 2;
|
||||
|
|
@ -28,7 +27,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 PositionNotFound();
|
||||
error PositionNotFound(uint256 positionId, address requester);
|
||||
|
||||
event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 share, uint32 creationTime, uint32 taxRate);
|
||||
event TaxPaid(uint256 indexed positionId, address indexed owner, uint256 taxAmount);
|
||||
|
|
@ -58,23 +57,19 @@ contract Stake is IStake {
|
|||
nextPositionId = 654321;
|
||||
}
|
||||
|
||||
function dormantSupply() public view override returns (uint256) {
|
||||
return totalSupply * (100 - MAX_STAKE) / 100;
|
||||
}
|
||||
|
||||
function authorizedStake() private view returns (uint256) {
|
||||
return totalSupply * MAX_STAKE / 100;
|
||||
}
|
||||
|
||||
function assetsToShares(uint256 assets, Math.Rounding rounding) private view returns (uint256) {
|
||||
return assets.mulDiv(totalSupply, harb.totalSupply(), rounding);
|
||||
function assetsToShares(uint256 assets) public view returns (uint256) {
|
||||
return assets.mulDiv(totalSupply, harb.totalSupply(), Math.Rounding.Down);
|
||||
//return assets.mulDiv(totalSupply, harb.totalSupply() + 1, rounding);
|
||||
}
|
||||
|
||||
function sharesToAssets(uint256 shares, Math.Rounding rounding) private view returns (uint256) {
|
||||
function sharesToAssets(uint256 shares) public view returns (uint256) {
|
||||
//return shares.mulDiv(harb.totalSupply() + 1, totalSupply, rounding);
|
||||
// TODO: should the average total supply be used for this calculation?
|
||||
return shares.mulDiv(harb.totalSupply(), totalSupply, rounding);
|
||||
return shares.mulDiv(harb.totalSupply(), totalSupply, Math.Rounding.Down);
|
||||
}
|
||||
|
||||
function permitAndSnatch(
|
||||
|
|
@ -108,9 +103,9 @@ contract Stake is IStake {
|
|||
returns (uint256 positionId)
|
||||
{
|
||||
// check lower boundary
|
||||
uint256 sharesWanted = assetsToShares(assets, Math.Rounding.Down);
|
||||
uint256 sharesWanted = assetsToShares(assets);
|
||||
{
|
||||
// check that position size is multiple of minStake
|
||||
// check that position size is at least minStake
|
||||
// to prevent excessive fragmentation, increasing snatch cost
|
||||
uint256 minStake = harb.previousTotalSupply() / 3000;
|
||||
if (sharesWanted < minStake) {
|
||||
|
|
@ -127,8 +122,7 @@ contract Stake is IStake {
|
|||
for (uint256 i = 0; i < positionsToSnatch.length - 1; i++) {
|
||||
StakingPosition storage pos = positions[positionsToSnatch[i]];
|
||||
if (pos.creationTime == 0) {
|
||||
//TODO:
|
||||
revert PositionNotFound();
|
||||
revert PositionNotFound(positionsToSnatch[i], receiver);
|
||||
}
|
||||
// check that tax lower
|
||||
if (taxRate <= pos.taxRate) {
|
||||
|
|
@ -151,7 +145,7 @@ contract Stake is IStake {
|
|||
StakingPosition storage lastPos = positions[positionsToSnatch[index]];
|
||||
if (lastPos.creationTime == 0) {
|
||||
//TODO:
|
||||
revert PositionNotFound();
|
||||
revert PositionNotFound(positionsToSnatch[index], receiver);
|
||||
}
|
||||
// check that tax lower
|
||||
if (taxRate <= lastPos.taxRate) {
|
||||
|
|
@ -199,72 +193,76 @@ contract Stake is IStake {
|
|||
emit PositionCreated(positionId, sp.owner, sp.share, sp.creationTime, sp.taxRate);
|
||||
}
|
||||
|
||||
function changeTax(uint256 positionID, uint32 taxRate) public {
|
||||
function changeTax(uint256 positionId, uint32 taxRate) public {
|
||||
require(taxRate < TAX_RATES.length, "tax rate out of bounds");
|
||||
StakingPosition storage pos = positions[positionID];
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
if (pos.creationTime == 0) {
|
||||
revert PositionNotFound(positionId, msg.sender);
|
||||
}
|
||||
if (pos.owner != msg.sender) {
|
||||
revert NoPermission(msg.sender, pos.owner);
|
||||
}
|
||||
//TODO: implement not found
|
||||
// to prevent snatch-and-change grieving attack, pay TAX_FLOOR_DURATION
|
||||
require(taxRate > pos.taxRate, "tax too low to snatch");
|
||||
_payTax(positionID, pos, TAX_FLOOR_DURATION);
|
||||
_payTax(positionId, pos, TAX_FLOOR_DURATION);
|
||||
pos.taxRate = taxRate;
|
||||
}
|
||||
|
||||
function exitPosition(uint256 positionId) public {
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
if (pos.creationTime == 0) {
|
||||
revert PositionNotFound(positionId, msg.sender);
|
||||
}
|
||||
if (pos.owner != msg.sender) {
|
||||
revert NoPermission(msg.sender, pos.owner);
|
||||
}
|
||||
//TODO: implement not found
|
||||
// to prevent snatch-and-exit grieving attack, pay TAX_FLOOR_DURATION
|
||||
_payTax(positionId, pos, TAX_FLOOR_DURATION);
|
||||
_exitPosition(positionId, pos);
|
||||
}
|
||||
|
||||
// TODO: write a bot that calls this function regularly
|
||||
function payTax(uint256 positionID) public {
|
||||
StakingPosition storage pos = positions[positionID];
|
||||
function payTax(uint256 positionId) public {
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
// TODO: what if someone calls payTax and exitPosition in the same transaction?
|
||||
_payTax(positionID, pos, 0);
|
||||
_payTax(positionId, pos, 0);
|
||||
}
|
||||
|
||||
function taxDue(uint256 positionID, uint256 taxFloorDuration) public view returns (uint256 amountDue) {
|
||||
StakingPosition storage pos = positions[positionID];
|
||||
function taxDue(uint256 positionId, uint256 taxFloorDuration) public view returns (uint256 amountDue) {
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
// ihet = Implied Holding Expiry Timestamp
|
||||
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration)
|
||||
? pos.creationTime + taxFloorDuration
|
||||
: block.timestamp;
|
||||
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
||||
uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down);
|
||||
uint256 assetsBefore = sharesToAssets(pos.share);
|
||||
amountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
||||
}
|
||||
|
||||
function _payTax(uint256 positionID, StakingPosition storage pos, uint256 taxFloorDuration) private {
|
||||
function _payTax(uint256 positionId, StakingPosition storage pos, uint256 taxFloorDuration) private {
|
||||
// ihet = Implied Holding Expiry Timestamp
|
||||
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration)
|
||||
? pos.creationTime + taxFloorDuration
|
||||
: block.timestamp;
|
||||
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
||||
uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down);
|
||||
uint256 assetsBefore = sharesToAssets(pos.share);
|
||||
uint256 taxAmountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
||||
if (taxAmountDue >= assetsBefore) {
|
||||
// can not pay more tax than value of position
|
||||
taxAmountDue = assetsBefore;
|
||||
}
|
||||
SafeERC20.safeTransfer(harb, taxPool, taxAmountDue);
|
||||
emit TaxPaid(positionID, pos.owner, taxAmountDue);
|
||||
emit TaxPaid(positionId, pos.owner, taxAmountDue);
|
||||
if (assetsBefore - taxAmountDue > 0) {
|
||||
// if something left over, update storage
|
||||
uint256 shareAfterTax = assetsToShares(assetsBefore - taxAmountDue, Math.Rounding.Down);
|
||||
uint256 shareAfterTax = assetsToShares(assetsBefore - taxAmountDue);
|
||||
outstandingStake -= pos.share - shareAfterTax;
|
||||
pos.share = shareAfterTax;
|
||||
pos.lastTaxTime = uint32(block.timestamp);
|
||||
} else {
|
||||
// if nothing left over, liquidate position
|
||||
outstandingStake -= pos.share;
|
||||
emit PositionRemoved(positionID, pos.share, pos.lastTaxTime);
|
||||
emit PositionRemoved(positionId, pos.share, pos.lastTaxTime);
|
||||
delete pos.owner;
|
||||
delete pos.creationTime;
|
||||
}
|
||||
|
|
@ -273,7 +271,7 @@ contract Stake is IStake {
|
|||
function _exitPosition(uint256 positionId, StakingPosition storage pos) private {
|
||||
outstandingStake -= pos.share;
|
||||
address owner = pos.owner;
|
||||
uint256 assets = sharesToAssets(pos.share, Math.Rounding.Down);
|
||||
uint256 assets = sharesToAssets(pos.share);
|
||||
emit PositionRemoved(positionId, pos.share, pos.lastTaxTime);
|
||||
delete pos.owner;
|
||||
delete pos.creationTime;
|
||||
|
|
@ -282,7 +280,7 @@ contract Stake is IStake {
|
|||
|
||||
function _shrinkPosition(uint256 positionId, StakingPosition storage pos, uint256 sharesToTake) private {
|
||||
require (sharesToTake < pos.share, "position too small");
|
||||
uint256 assets = sharesToAssets(sharesToTake, Math.Rounding.Down);
|
||||
uint256 assets = sharesToAssets(sharesToTake);
|
||||
pos.share -= sharesToTake;
|
||||
emit PositionShrunk(positionId, pos.share, pos.lastTaxTime, sharesToTake);
|
||||
SafeERC20.safeTransfer(harb, pos.owner, assets);
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
interface IStake {
|
||||
function dormantSupply() external view returns (uint256);
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ contract BaseLineLPTest is PoolSerializer {
|
|||
weth = IWETH9(address(new WETH()));
|
||||
|
||||
TwabController tc = new TwabController(60 * 60 * 24, uint32(block.timestamp));
|
||||
harb = new Harb("HARB", "HARB", factoryAddress, address(weth), tc);
|
||||
harb = new Harb("HARB", "HARB", tc);
|
||||
|
||||
pool = IUniswapV3Pool(factory.createPool(address(weth), address(harb), FEE));
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue