wip
This commit is contained in:
parent
854c26a623
commit
307f98840b
6 changed files with 42 additions and 31 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
pragma solidity ^0.8.4;
|
pragma solidity ^0.8.4;
|
||||||
|
|
||||||
import "forge-std/Script.sol";
|
import "forge-std/Script.sol";
|
||||||
|
import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
|
||||||
import "../src/Harb.sol";
|
import "../src/Harb.sol";
|
||||||
import "../src/Stake.sol";
|
import "../src/Stake.sol";
|
||||||
|
|
||||||
|
|
@ -12,7 +13,8 @@ contract GoerliScript is Script {
|
||||||
uint256 privateKey = vm.deriveKey(seedPhrase, 0);
|
uint256 privateKey = vm.deriveKey(seedPhrase, 0);
|
||||||
vm.startBroadcast(privateKey);
|
vm.startBroadcast(privateKey);
|
||||||
|
|
||||||
Harb harb = new Harb("Harberger Tax", "HARB");
|
TwabController tc = new TwabController(60*60*24, uint32(block.timestamp));
|
||||||
|
Harb harb = new Harb("Harberger Tax", "HARB", tc);
|
||||||
Stake stake = new Stake(address(harb));
|
Stake stake = new Stake(address(harb));
|
||||||
harb.setStakingPool(address(stake));
|
harb.setStakingPool(address(stake));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,14 +72,14 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
/// @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) external onlyLiquidityManager {
|
function mint(uint256 _amount) external onlyLiquidityManager {
|
||||||
_mintHarb(liquidityManager, _amount);
|
_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) external onlyLiquidityManager {
|
function burn(uint256 _amount) external onlyLiquidityManager {
|
||||||
_burnHarb(liquidityManager, _amount);
|
_burn(liquidityManager, _amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============ Public ERC20 Overrides ============ */
|
/* ============ Public ERC20 Overrides ============ */
|
||||||
|
|
@ -107,10 +107,10 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
* @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 _mintHarb(address receiver, uint256 amount) internal {
|
function _mint(address receiver, uint256 amount) internal virtual override {
|
||||||
// make sure staking pool grows proportional to economy
|
// make sure staking pool grows proportional to economy
|
||||||
uint256 stakingPoolBalance = balanceOf(stakingPool);
|
uint256 stakingPoolBalance = balanceOf(stakingPool);
|
||||||
uint256 activeSupply = totalSupply - stakingPoolBalance;
|
uint256 activeSupply = totalSupply() - stakingPoolBalance;
|
||||||
uint256 dormantStake = IStake(stakingPool).dormantSupply();
|
uint256 dormantStake = IStake(stakingPool).dormantSupply();
|
||||||
if (stakingPoolBalance > 0) {
|
if (stakingPoolBalance > 0) {
|
||||||
uint256 newStake = stakingPoolBalance * amount / (activeSupply + dormantStake);
|
uint256 newStake = stakingPoolBalance * amount / (activeSupply + dormantStake);
|
||||||
|
|
@ -129,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 _burnHarb(address _owner, uint256 _amount) internal {
|
function _burn(address _owner, uint256 _amount) internal virtual override {
|
||||||
// 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);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ 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 '@uniswap-v3-core/interfaces/IUniswapV3Pool.sol';
|
||||||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
import '@openzeppelin/token/ERC20/IERC20.sol';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title LiquidityManager - A contract that supports the $bloodX ecosystem. It
|
* @title LiquidityManager - A contract that supports the $bloodX ecosystem. It
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ contract Stake is IStake {
|
||||||
}
|
}
|
||||||
|
|
||||||
uint256 public immutable totalSupply;
|
uint256 public immutable totalSupply;
|
||||||
address private immutable tokenContract;
|
IERC20 private immutable tokenContract;
|
||||||
address private immutable taxPool;
|
address private immutable taxPool;
|
||||||
uint256 public outstandingStake;
|
uint256 public outstandingStake;
|
||||||
uint256 private lastTokenId;
|
uint256 private lastTokenId;
|
||||||
|
|
@ -43,7 +43,7 @@ contract Stake is IStake {
|
||||||
constructor(
|
constructor(
|
||||||
address _tokenContract
|
address _tokenContract
|
||||||
) {
|
) {
|
||||||
tokenContract = _tokenContract;
|
tokenContract = IERC20(_tokenContract);
|
||||||
IHarb harb = IHarb(_tokenContract);
|
IHarb harb = IHarb(_tokenContract);
|
||||||
totalSupply = 100 * 10 ** 5 * harb.decimals();
|
totalSupply = 100 * 10 ** 5 * harb.decimals();
|
||||||
taxPool = harb.taxPool();
|
taxPool = harb.taxPool();
|
||||||
|
|
@ -58,11 +58,11 @@ contract Stake is IStake {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 / 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 * tokenContract.totalSupply() / totalSupply;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,7 +73,7 @@ contract Stake is IStake {
|
||||||
* paying for execution may not be the actual sender (as far as an application
|
* paying for execution may not be the actual sender (as far as an application
|
||||||
* is concerned).
|
* is concerned).
|
||||||
*/
|
*/
|
||||||
function snatch(uint256 assets, address receiver, uint64 taxRate, uint256[] calldata positionsToSnatch) public returns(uint256) {
|
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) public returns(uint256) {
|
||||||
|
|
||||||
// check lower boundary
|
// check lower boundary
|
||||||
uint256 sharesWanted = assetsToShares(assets);
|
uint256 sharesWanted = assetsToShares(assets);
|
||||||
|
|
@ -89,15 +89,16 @@ contract Stake is IStake {
|
||||||
revert PositionNotFound();
|
revert PositionNotFound();
|
||||||
}
|
}
|
||||||
// check that tax lower
|
// check that tax lower
|
||||||
if (taxRate <= pos.perSecondTaxRate) {
|
if (taxRate <= pos.taxRate) {
|
||||||
revert TaxTooLow(receiver, taxRate, pos.perSecondTaxRate, i);
|
revert TaxTooLow(receiver, taxRate, pos.taxRate, i);
|
||||||
}
|
}
|
||||||
// dissolve position
|
// dissolve position
|
||||||
_payTax(pos);
|
// TODO: what if someone calls payTax and exitPosition in the same transaction?
|
||||||
|
_payTax(pos, 0);
|
||||||
_exitPosition(pos);
|
_exitPosition(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// now try to make a new position in the free space and hope it is big enough
|
// 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);
|
||||||
|
|
@ -110,9 +111,9 @@ contract Stake is IStake {
|
||||||
StakingPosition storage sp = positions[lastTokenId++];
|
StakingPosition storage sp = positions[lastTokenId++];
|
||||||
sp.share = sharesWanted;
|
sp.share = sharesWanted;
|
||||||
sp.owner = receiver;
|
sp.owner = receiver;
|
||||||
sp.lastTaxTime = now;
|
sp.lastTaxTime = uint32(block.timestamp);
|
||||||
sp.creationTime = now;
|
sp.creationTime = uint32(block.timestamp);
|
||||||
sp.perSecondTaxRate = taxRate;
|
sp.taxRate = taxRate;
|
||||||
|
|
||||||
outstandingStake += sharesWanted;
|
outstandingStake += sharesWanted;
|
||||||
|
|
||||||
|
|
@ -129,17 +130,17 @@ contract Stake is IStake {
|
||||||
_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 storage pos = positions[positionID];
|
StakingPosition storage pos = positions[positionID];
|
||||||
|
// TODO: what if someone calls payTax and exitPosition in the same transaction?
|
||||||
_payTax(pos, 0);
|
_payTax(pos, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function _payTax(StakingPosition storage pos, uint256 taxFloorDuration) private {
|
function _payTax(StakingPosition storage pos, uint256 taxFloorDuration) private {
|
||||||
// ihet = Implied Holding Expiry Timestamp
|
// ihet = Implied Holding Expiry Timestamp
|
||||||
uint256 ihet = (now - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : now;
|
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp;
|
||||||
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
||||||
uint256 assetsBefore = sharesToAssets(pos.share);
|
uint256 assetsBefore = sharesToAssets(pos.share);
|
||||||
uint256 taxDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
uint256 taxDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
||||||
|
|
@ -150,13 +151,14 @@ 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
|
||||||
pos.shares = assetsToShares(assetsBefore - taxDue);
|
pos.share = assetsToShares(assetsBefore - taxDue);
|
||||||
pos.lastTaxTime = now;
|
pos.lastTaxTime = uint32(block.timestamp);
|
||||||
} else {
|
} else {
|
||||||
// if nothing left over, liquidate position
|
// if nothing left over, liquidate position
|
||||||
// TODO: emit event
|
// TODO: emit event
|
||||||
outstandingStake -= pos.share;
|
outstandingStake -= pos.share;
|
||||||
delete pos;
|
delete pos.owner;
|
||||||
|
delete pos.creationTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,7 +166,8 @@ contract Stake is IStake {
|
||||||
outstandingStake -= pos.share;
|
outstandingStake -= pos.share;
|
||||||
address owner = pos.owner;
|
address owner = pos.owner;
|
||||||
uint256 assets = sharesToAssets(pos.share);
|
uint256 assets = sharesToAssets(pos.share);
|
||||||
delete pos;
|
delete pos.owner;
|
||||||
|
delete pos.creationTime;
|
||||||
SafeERC20.safeTransfer(tokenContract, owner, assets);
|
SafeERC20.safeTransfer(tokenContract, owner, assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
pragma solidity ^0.8.13;
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
interface IHarb {
|
import { IERC20Metadata } from "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
|
|
||||||
|
interface IHarb is IERC20Metadata {
|
||||||
|
|
||||||
function taxPool() external view returns(address);
|
function taxPool() external view returns(address);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ pragma solidity ^0.8.13;
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "forge-std/Test.sol";
|
||||||
import "forge-std/console.sol";
|
import "forge-std/console.sol";
|
||||||
|
import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
|
||||||
import "../src/Harb.sol";
|
import "../src/Harb.sol";
|
||||||
import "../src/Stake.sol";
|
import "../src/Stake.sol";
|
||||||
|
|
||||||
|
|
@ -12,7 +13,10 @@ contract BloodXTest is Test {
|
||||||
uint256 constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
|
uint256 constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
|
||||||
|
|
||||||
function setUp() public {
|
function setUp() public {
|
||||||
harb = new Harb("name", "SYM");
|
|
||||||
|
TwabController tc = new TwabController(60*60*24, uint32(block.timestamp));
|
||||||
|
|
||||||
|
harb = new Harb("name", "SYM", tc);
|
||||||
stake = new Stake(address(harb));
|
stake = new Stake(address(harb));
|
||||||
harb.setStakingPool(address(stake));
|
harb.setStakingPool(address(stake));
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +31,7 @@ contract BloodXTest is Test {
|
||||||
// test mint
|
// test mint
|
||||||
uint256 totalSupplyBefore = harb.totalSupply();
|
uint256 totalSupplyBefore = harb.totalSupply();
|
||||||
uint256 balanceBefore = harb.balanceOf(account);
|
uint256 balanceBefore = harb.balanceOf(account);
|
||||||
harb.purchase(account, amount);
|
harb.mint(amount);
|
||||||
uint256 totalAfter = harb.totalSupply();
|
uint256 totalAfter = harb.totalSupply();
|
||||||
assertEq(totalAfter, totalSupplyBefore + amount, "total supply should match");
|
assertEq(totalAfter, totalSupplyBefore + amount, "total supply should match");
|
||||||
assertEq(harb.balanceOf(account), balanceBefore + amount, "balance should match");
|
assertEq(harb.balanceOf(account), balanceBefore + amount, "balance should match");
|
||||||
|
|
@ -36,13 +40,13 @@ contract BloodXTest is Test {
|
||||||
uint256 newStake = amount / 2 * 100000 ether / totalAfter;
|
uint256 newStake = amount / 2 * 100000 ether / totalAfter;
|
||||||
{
|
{
|
||||||
uint256 outstandingBefore = stake.totalSupply();
|
uint256 outstandingBefore = stake.totalSupply();
|
||||||
uint256 stakeBalanceBefore = stake.balanceOf(account);
|
(,,,,uint256 stakeBalanceBefore) = stake.positions(1);
|
||||||
vm.prank(account);
|
vm.prank(account);
|
||||||
harb.stake(account, amount / 2);
|
harb.stake(account, amount / 2);
|
||||||
assertEq(harb.totalSupply(), totalSupplyBefore + (amount - (amount / 2)), "total supply should match after stake");
|
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(harb.balanceOf(account), balanceBefore + (amount - (amount / 2)), "balance should match after stake");
|
||||||
assertEq(outstandingBefore + newStake, stake.totalSupply(), "outstanding supply should match");
|
assertEq(outstandingBefore + newStake, stake.totalSupply(), "outstanding supply should match");
|
||||||
assertEq(stakeBalanceBefore + newStake, stake.balanceOf(account), "balance of stake account should match");
|
assertEq(stakeBalanceBefore + newStake, stake.positions(1).share, "balance of stake account should match");
|
||||||
}
|
}
|
||||||
|
|
||||||
// test unstake
|
// test unstake
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue