From 94c4ff05d4a03fb4ad370142bec6baf005b67084 Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Fri, 23 Feb 2024 22:01:23 +0100 Subject: [PATCH] rebased on PTv5 --- remappings.txt | 2 +- script/Deploy.sol | 10 +- src/BloodX.sol | 159 ------------------- src/ERC1363.sol | 131 ---------------- src/ERC20.sol | 316 -------------------------------------- src/Harb.sol | 151 ++++++++++++++++++ src/Stake.sol | 57 ++++--- src/interfaces/IHarb.sol | 9 ++ src/interfaces/IStake.sol | 3 +- 9 files changed, 196 insertions(+), 642 deletions(-) delete mode 100644 src/BloodX.sol delete mode 100644 src/ERC1363.sol delete mode 100644 src/ERC20.sol create mode 100644 src/Harb.sol create mode 100644 src/interfaces/IHarb.sol diff --git a/remappings.txt b/remappings.txt index f1b3607..af07f81 100644 --- a/remappings.txt +++ b/remappings.txt @@ -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/ diff --git a/script/Deploy.sol b/script/Deploy.sol index 72de3b9..5be4cc0 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -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(); } diff --git a/src/BloodX.sol b/src/BloodX.sol deleted file mode 100644 index 99b187e..0000000 --- a/src/BloodX.sol +++ /dev/null @@ -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); - } - - -} diff --git a/src/ERC1363.sol b/src/ERC1363.sol deleted file mode 100644 index f7d9e8f..0000000 --- a/src/ERC1363.sol +++ /dev/null @@ -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)) - } - } - } - } -} \ No newline at end of file diff --git a/src/ERC20.sol b/src/ERC20.sol deleted file mode 100644 index 55dfc41..0000000 --- a/src/ERC20.sol +++ /dev/null @@ -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); - } - } - } -} \ No newline at end of file diff --git a/src/Harb.sol b/src/Harb.sol new file mode 100644 index 0000000..b10e1b5 --- /dev/null +++ b/src/Harb.sol @@ -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); + } + +} \ No newline at end of file diff --git a/src/Stake.sol b/src/Stake.sol index a715d30..032a34e 100644 --- a/src/Stake.sol +++ b/src/Stake.sol @@ -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); } diff --git a/src/interfaces/IHarb.sol b/src/interfaces/IHarb.sol new file mode 100644 index 0000000..0d75000 --- /dev/null +++ b/src/interfaces/IHarb.sol @@ -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); + +} diff --git a/src/interfaces/IStake.sol b/src/interfaces/IStake.sol index 2975fe9..2866dcd 100644 --- a/src/interfaces/IStake.sol +++ b/src/interfaces/IStake.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-3.0-or-later + pragma solidity ^0.8.13; interface IStake {