rebased on PTv5

This commit is contained in:
JulesCrown 2024-02-23 22:01:23 +01:00
parent dea9e0704a
commit 94c4ff05d4
9 changed files with 196 additions and 642 deletions

View file

@ -1,4 +1,4 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
@openzeppelin/=lib/openzeppelin-contracts/contracts/
@uniswap/v3-core/=lib/v3-core/
@uniswap/v3-periphery/=lib/v3-periphery/
@aperture/uni-v3-lib/=lib/uni-v3-lib/src/

View file

@ -1,8 +1,8 @@
pragma solidity ^0.8.4;
import "forge-std/Script.sol";
import "../src/BloodX.sol";
import "../src/StakeX.sol";
import "../src/Harb.sol";
import "../src/Stake.sol";
contract GoerliScript is Script {
function setUp() public {}
@ -12,9 +12,9 @@ contract GoerliScript is Script {
uint256 privateKey = vm.deriveKey(seedPhrase, 0);
vm.startBroadcast(privateKey);
BloodX bloodX = new BloodX("bloodX", "bXXX");
StakeX stakeX = new StakeX("stakeX", "sXXX", address(bloodX));
bloodX.setStakingContract(address(stakeX));
Blood bloodX = new BloodX("bloodX", "bXXX");
Stake stakeX = new StakeX(address(bloodX));
blood.setStakingContract(address(stakeX));
vm.stopBroadcast();
}

View file

@ -1,159 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;
import "./ERC1363.sol";
import "./interfaces/IERC1363Receiver.sol";
import "./interfaces/IStakeX.sol";
contract BloodX is ERC1363, IERC1363Receiver {
address public stakingContract;
constructor(
string memory name,
string memory symbol
) ERC20(name, symbol) {
// nothing yet
}
/**
* @dev trapdoor function to set dependency to staking contract
* @param _stakingContract address enables staking and unstaking
*/
function setStakingContract(address _stakingContract) external {
require (stakingContract == address(0), "already set");
stakingContract = _stakingContract;
}
/**
* @dev getter for the size of the pool that manages all staked $bloodX tokens
* @return the amount of $bloodX token currently being staked
*/
function stakingPool() public view returns(uint256) {
return balanceOf(stakingContract);
}
/**
* @dev getter for the size of active supply
* active supply is defined as all tokens in circulation, not bound in the staking pool.
* @return the amount of $bloodX token currently in circulation
*/
function activeSupply() public view returns(uint256) {
return super.totalSupply() - balanceOf(stakingContract);
}
/**
* @dev getter for total supply, synonym for activeSupply
* @return the amount of $bloodX token currently in circulation
*/
function totalSupply() public view override(ERC20, IERC20) returns(uint256) {
return activeSupply();
}
/**
* @dev This function is called after the blood bond. It mints tokens to the user
* and bumps up the supply in the staking pool.
* TODO: implement authentication with caller
* TODO: what about approve/allowance workflow?
* TODO: return percentage of supply?
* @param receiver is the address of the user who initiated the blood bond
* @param amount of $bloodX tokens minted to the user during blood bond
*/
function purchase(address receiver, uint256 amount) external {
if (receiver == stakingContract || receiver == address(this) || receiver == address(0)) {
revert ERC20InvalidReceiver(receiver);
}
require(amount > 0);
// make sure staking pool grows proportional to economy
uint256 stakingPoolBalance = stakingPool();
uint256 dormantStake = IStakeX(stakingContract).dormantSupply();
if (stakingPoolBalance > 0) {
uint256 newStake = stakingPoolBalance * amount / (activeSupply() + dormantStake);
_mint(stakingContract, newStake);
}
_mint(receiver, amount);
}
/**
* @dev stake immobilizes tokens by pulling them out of active supply and storing
* them in the staking pool. The distribution in the staking pool is managed by
* the staking contract, a separate ERC20. an ERC1363 call is executed to notify the
* staking contract and update the shares.
* TODO: return something?
* @param account address is the address of the beneficiary of the staking operation
* @param value uint256 is the amount of $bloodX tokens that should be moved to staking
* TODO: should there be a minimum amount for staking?
*/
function stake(address account, uint256 value) external {
address spender = _msgSender();
if (account == stakingContract || account == address(this) || account == address(0)) {
revert ERC20InvalidReceiver(account);
}
if (account != spender) {
_spendAllowance(account, spender, value);
}
_update(account, stakingContract, value);
bytes memory data = new bytes(32);
uint256 overall = super.totalSupply();
assembly { mstore(add(data, 32), overall) }
// take staked tokens out of supply on event level
emit Transfer(account, address(0), value);
_checkOnTransferReceived(address(this), account, stakingContract, value, data);
}
/**
* @dev See {IERC1363Receiver-onTransferReceived}.
*/
function onTransferReceived(
address operator,
address from,
uint256 value,
bytes memory data
) external override returns (bytes4) {
if (data.length == 1) {
if (data[0] == 0x00) return bytes4(0);
if (data[0] == 0x01) revert("onTransferReceived revert");
if (data[0] == 0x02) revert();
if (data[0] == 0x03) assert(false);
}
if (operator == stakingContract) {
// a user has initiated unstaking
require(data.length <= 32, "The byte array is too long");
_unstake(from, value, uint256(bytes32(data)));
} else {
emit Received(operator, from, value, data);
}
return this.onTransferReceived.selector;
}
/**
* @dev See {ERC20-_update}.
* Note: emission of Transfer event customized in super function
*/
function _update(address from, address to, uint256 value) internal override {
super._update(from, to, value);
// don't emit transfer event when updating staking pool.
if (from != stakingContract && to != stakingContract) {
emit Transfer(from, to, value);
}
}
/**
* @dev Internal function to unstake tokens from the staking pool
*/
function _unstake(address account, uint256 value, uint256 authorizedSupply) internal {
uint256 amount = value * super.totalSupply() / authorizedSupply;
// transfer tokens
_update(stakingContract, account, amount);
// introduce unstaked tokens as mint on Event level
emit Transfer(address(0), account, amount);
}
}

View file

@ -1,131 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "./ERC20.sol";
import "./interfaces/IERC1363.sol";
import "./interfaces/IERC1363Receiver.sol";
import "./interfaces/IERC1363Spender.sol";
abstract contract ERC1363 is IERC1363, ERC20, ERC165 {
using Address for address;
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
return interfaceId == type(IERC1363).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev See {IERC1363-transferAndCall}.
*/
function transferAndCall(address to, uint256 value) public override returns (bool) {
return transferAndCall(to, value, bytes(""));
}
/**
* @dev See {IERC1363-transferAndCall}.
*/
function transferAndCall(address to, uint256 value, bytes memory data) public override returns (bool) {
require(transfer(to, value));
require(
_checkOnTransferReceived(_msgSender(), _msgSender(), to, value, data),
"ERC1363: transfer to non ERC1363Receiver implementer"
);
return true;
}
/**
* @dev See {IERC1363-transferFromAndCall}.
*/
function transferFromAndCall(address from, address to, uint256 value) public override returns (bool) {
return transferFromAndCall(from, to, value, bytes(""));
}
/**
* @dev See {IERC1363-transferFromAndCall}.
*/
function transferFromAndCall(
address from,
address to,
uint256 value,
bytes memory data
) public override returns (bool) {
require(transferFrom(from, to, value));
require(
_checkOnTransferReceived(_msgSender(), from, to, value, data),
"ERC1363: transfer to non ERC1363Receiver implementer"
);
return true;
}
/**
* @dev See {IERC1363-approveAndCall}.
*/
function approveAndCall(address spender, uint256 value) public override returns (bool) {
return approveAndCall(spender, value, bytes(""));
}
/**
* @dev See {IERC1363-approveAndCall}.
*/
function approveAndCall(address spender, uint256 value, bytes memory data) public override returns (bool) {
require(approve(spender, value));
require(
_checkOnApprovalReceived(_msgSender(), spender, value, data),
"ERC1363: transfer to non ERC1363Spender implementer"
);
return true;
}
/**
* @dev Internal function to invoke {IERC1363Receiver-onTransferReceived} on a target address.
* The call is not executed if the target address is not a contract.
*/
function _checkOnTransferReceived(
address operator,
address from,
address to,
uint256 value,
bytes memory data
) internal returns (bool) {
try IERC1363Receiver(to).onTransferReceived(operator, from, value, data) returns (bytes4 retval) {
return retval == IERC1363Receiver.onTransferReceived.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC1363: transfer to non ERC1363Receiver implementer");
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
/**
* @dev Internal function to invoke {IERC1363Spender-onApprovalReceived} on a target address.
* The call is not executed if the target address is not a contract.
*/
function _checkOnApprovalReceived(
address owner,
address spender,
uint256 value,
bytes memory data
) private returns (bool) {
try IERC1363Spender(spender).onApprovalReceived(owner, value, data) returns (bytes4 retval) {
return retval == IERC1363Spender.onApprovalReceived.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC1363: transfer to non ERC1363Spender implementer");
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
}

View file

@ -1,316 +0,0 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
/**
* @dev Implementation of the {IERC20} interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using {_mint}.
*
* TIP: For a detailed writeup see our guide
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* The default value of {decimals} is 18. To change this, you should override
* this function so it returns a different value.
*
* We have followed general OpenZeppelin Contracts guidelines: functions revert
* instead returning `false` on failure. This behavior is nonetheless
* conventional and does not conflict with the expectations of ERC20
* applications.
*
* Additionally, an {Approval} event is emitted on calls to {transferFrom}.
* This allows applications to reconstruct the allowance for all accounts just
* by listening to said events. Other implementations of the EIP may not emit
* these events, as it isn't required by the specification.
*/
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
mapping(address account => uint256) private _balances;
mapping(address account => mapping(address spender => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
/**
* @dev Sets the values for {name} and {symbol}.
*
* All two of these values are immutable: they can only be set once during
* construction.
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev Returns the name of the token.
*/
function name() public view virtual returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual returns (string memory) {
return _symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the default value returned by this function, unless
* it's overridden.
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual returns (uint8) {
return 18;
}
/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}
/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view virtual returns (uint256) {
return _balances[account];
}
/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - the caller must have a balance of at least `value`.
*/
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev See {IERC20-approve}.
*
* NOTE: If `value` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, value);
return true;
}
/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* NOTE: Does not update the allowance if the current allowance
* is the maximum `uint256`.
*
* Requirements:
*
* - `from` and `to` cannot be the zero address.
* - `from` must have a balance of at least `value`.
* - the caller must have allowance for ``from``'s tokens of at least
* `value`.
*/
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}
/**
* @dev Moves a `value` amount of tokens from `from` to `to`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
/**
* @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
* this function.
*
* Emits a {Transfer} event.
*/
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
_balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
_totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
_balances[to] += value;
}
}
// emit Transfer(from, to, value);
}
/**
* @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
* Relies on the `_update` mechanism
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(address(0), account, value);
}
/**
* @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.
* Relies on the `_update` mechanism.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead
*/
function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(account, address(0), value);
}
/**
* @dev Sets `value` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*
* Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
*/
function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}
/**
* @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.
*
* By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by
* `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any
* `Approval` event during `transferFrom` operations.
*
* Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to
* true using the following override:
* ```
* function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
* super._approve(owner, spender, value, true);
* }
* ```
*
* Requirements are the same as {_approve}.
*/
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}
/**
* @dev Updates `owner` s allowance for `spender` based on spent `value`.
*
* Does not update the allowance value in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Does not emit an {Approval} event.
*/
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}
}

151
src/Harb.sol Normal file
View file

@ -0,0 +1,151 @@
// SPDX-License-Identifier: GPL-3.0-or-later
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 { TwabController } from "pt-v5-twab-controller/TwabController.sol";
/**
* @title TWAB ERC20 Token
* @notice This contract creates an ERC20 token with balances stored in a TwabController,
* enabling time-weighted average balances for each holder.
* @dev The TwabController limits all balances including total token supply to uint96 for
* gas savings. Any mints that increase a balance past this limit will fail.
*/
contract Harb is ERC20, ERC20Permit {
/* ============ Public Variables ============ */
/// @notice Address of the TwabController used to keep track of balances.
TwabController public immutable twabController;
/// @notice Address of the LiquidityManager that mints and burns supply
address public immutable liquidityManager;
/* ============ Errors ============ */
/// @notice Thrown if the some address is unexpectedly the zero address.
error ZeroAddressInConstructor();
/// @dev Function modifier to ensure that the caller is the liquidityManager
modifier onlyLiquidityManager() {
require(msg.sender == liquidityManager, "Harb/only-lm");
_;
}
/* ============ Constructor ============ */
/**
* @notice TwabERC20 Constructor
* @param name_ The name of the token
* @param symbol_ The token symbol
*/
constructor(
string memory name_,
string memory symbol_,
TwabController twabController_,
address liquidityManager_
) ERC20(name_, symbol_) ERC20Permit(name_) {
if (address(0) == address(twabController_)) revert ZeroAddressInConstructor();
twabController = twabController_;
if (address(0) == liquidityManager_) revert ZeroAddressInConstructor();
liquidityManager = liquidityManager_;
}
/* ============ 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);
}
/// @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);
}
/* ============ Public ERC20 Overrides ============ */
/// @inheritdoc ERC20
function balanceOf(
address _account
) public view virtual override(ERC20) returns (uint256) {
return twabController.balanceOf(address(this), _account);
}
/// @inheritdoc ERC20
function totalSupply() public view virtual override(ERC20) returns (uint256) {
return twabController.totalSupply(address(this));
}
/* ============ Internal ERC20 Overrides ============ */
/**
* @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
*/
function _mint(address _receiver, uint256 _amount) internal virtual override {
// make sure staking pool grows proportional to economy
uint256 stakingPoolBalance = stakingPool();
uint256 dormantStake = IStakeX(stakingContract).dormantSupply();
if (stakingPoolBalance > 0) {
uint256 newStake = stakingPoolBalance * amount / (activeSupply() + dormantStake);
_mint(stakingContract, newStake);
}
twabController.mint(_receiver, SafeCast.toUint96(_amount));
emit Transfer(address(0), _receiver, _amount);
}
/**
* @notice Destroys tokens from `_owner` and reduces the total supply.
* @dev Emits a {Transfer} event with `to` set to the zero address.
* @dev `_owner` cannot be the zero address.
* @dev `_owner` must have at least `_amount` tokens.
* @param _owner The owner of the tokens
* @param _amount The amount of tokens to burn
*/
function _burn(address _owner, uint256 _amount) internal virtual override {
// TODO
twabController.burn(_owner, SafeCast.toUint96(_amount));
emit Transfer(_owner, address(0), _amount);
}
/**
* @notice Transfers tokens from one account to another.
* @dev Emits a {Transfer} event.
* @dev `_from` cannot be the zero address.
* @dev `_to` cannot be the zero address.
* @dev `_from` must have a balance of at least `_amount`.
* @param _from Address to transfer from
* @param _to Address to transfer to
* @param _amount The amount of tokens to transfer
*/
function _transfer(address _from, address _to, uint256 _amount) internal virtual override {
twabController.transfer(_from, _to, SafeCast.toUint96(_amount));
emit Transfer(_from, _to, _amount);
}
}

View file

@ -1,4 +1,5 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;
import "./interfaces/IStake.sol";
@ -6,14 +7,10 @@ import "./interfaces/IHarb.sol";
contract Stake is IStake {
// when ustaking, at least authorizedSupply/minUnstake stake should be claimed
uint256 internal constant MAX_STAKE = 20; // 20% of HARB supply
uint256 internal constant MAX_TAX = 1000; // max 1000% tax
uint256 internal constant TAX_RATE_BASE = 100;
uint256 public immutable totalSupply;
address private immutable tokenContract;
address private immutable taxPool;
uint256 internal constant TAX_FLOOR_DURATION = 60 * 60 * 24 * 3; //this duration is the minimum basis for fee calculation, regardless of actual holding time.
/**
* @dev Attempted to deposit more assets than the max amount for `receiver`.
*/
@ -21,17 +18,19 @@ 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 ExitTooEarly(address owner, uint256 positionID, uint32 creationTimestamp);
struct StakingPosition {
uint256 stakeShare;
uint256 share;
address owner;
uint32 creationTimestamp;
uint32 lastTaxPaymentTimestamp;
uint32 taxRate; // value of 60 = 60%
uint32 creationTime;
uint32 lastTaxTime;
uint32 taxRate; // e.g. value of 60 = 60% tax per year
}
uint256 public immutable totalSupply;
address private immutable tokenContract;
address private immutable taxPool;
uint256 public outstandingStake;
uint256 private lastTokenId;
uint256 public minStake;
@ -39,10 +38,8 @@ contract Stake is IStake {
constructor(
string memory name,
string memory symbol,
address _tokenContract
) ERC20(name, symbol) {
) {
tokenContract = _tokenContract;
IHarb harb = IHarb(_tokenContract);
totalSupply = 100 * 10 ** 5 * harb.decimals();
@ -92,10 +89,10 @@ contract Stake is IStake {
// mint
StakingPosition storage sp = c.funders[lastTokenId++];
sp.stakeShare = shares;
sp.share = shares;
sp.owner = receiver;
sp.lastTaxPaymentTimestamp = now;
sp.creationTimestamp = now;
sp.lastTaxTime = now;
sp.creationTime = now;
sp.perSecondTaxRate = taxRate;
outstandingStake += sharesWanted;
@ -110,23 +107,25 @@ contract Stake is IStake {
NoPermission(_msgSender(), pos.owner);
}
// to prevent snatch-and-exit grieving attack
if(now - pos.creationTimestamp < 60 * 60 * 24 * 3) {
ExitTooEarly(pos.owner, positionID, pos.creationTimestamp);
if(now - pos.creationTime < 60 * 60 * 24 * 3) {
ExitTooEarly(pos.owner, positionID, pos.creationTime);
}
_payTax(pos);
_payTax(pos, TAX_FLOOR_DURATION);
_exitPosition(pos);
}
function payTax(uint256 positionID) public {
StakingPosition pos = positions[positionID];
_payTax(pos);
_payTax(pos, 0);
}
function _payTax(StakingPosition storage pos) private {
uint256 elapsedTime = now - pos.lastTaxPaymentTimestamp;
uint256 assetsBefore = sharesToAssets(pos.stakeShare);
function _payTax(StakingPosition storage pos, uint256 taxFloorDuration) private {
// ihet = Implied Holding Expiry Timestamp
uint256 ihet = (now - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : now;
uint256 elapsedTime = ihet - pos.lastTaxTime;
uint256 assetsBefore = sharesToAssets(pos.share);
uint256 taxDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
if (taxDue >= assetsBefore) {
// can not pay more tax than value of position
@ -135,19 +134,19 @@ contract Stake is IStake {
SafeERC20.safeTransfer(tokenContract, taxPool, taxDue);
if (assetsBefore - taxDue > 0) {
// if something left over, update storage
sp.stakeShares = assetsToShares(assetsBefore - taxDue);
sp.lastTaxPaymentTimestamp = now;
sp.shares = assetsToShares(assetsBefore - taxDue);
sp.lastTaxTime = now;
} else {
// if nothing left over, liquidate position
outstandingStake -= sp.stakeShare;
outstandingStake -= sp.share;
delete sp;
}
}
function _exitPosition(StakingPosition storage pos) private {
outstandingStake -= pos.stakeShare;
outstandingStake -= pos.share;
address owner = pos.owner;
uint256 assets = sharesToAssets(pos.stakeShare);
uint256 assets = sharesToAssets(pos.share);
delete pos;
SafeERC20.safeTransfer(tokenContract, owner, assets);
}

9
src/interfaces/IHarb.sol Normal file
View file

@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;
interface IHarb {
function taxPool() external view returns(address);
}

View file

@ -1,4 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;
interface IStake {