From 5711f24e988b5001def41d1e42c75ee57e81bbf0 Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Tue, 21 Nov 2023 20:27:49 +0100 Subject: [PATCH] initial commit --- .github/workflows/test.yml | 34 --- .gitmodules | 3 + foundry.toml | 4 +- lib/openzeppelin-contracts | 1 + remappings.txt | 1 + src/BloodX.sol | 159 ++++++++++++++ src/Counter.sol | 14 -- src/ERC1363.sol | 131 ++++++++++++ src/ERC20.sol | 316 ++++++++++++++++++++++++++++ src/StakeX.sol | 173 +++++++++++++++ src/interfaces/IERC1363.sol | 73 +++++++ src/interfaces/IERC1363Receiver.sol | 35 +++ src/interfaces/IERC1363Spender.sol | 26 +++ src/interfaces/IStakeX.sol | 12 ++ test/BloodX.t.sol | 65 ++++++ test/Counter.t.sol | 24 --- 16 files changed, 998 insertions(+), 73 deletions(-) delete mode 100644 .github/workflows/test.yml create mode 160000 lib/openzeppelin-contracts create mode 100644 remappings.txt create mode 100644 src/BloodX.sol delete mode 100644 src/Counter.sol create mode 100644 src/ERC1363.sol create mode 100644 src/ERC20.sol create mode 100644 src/StakeX.sol create mode 100644 src/interfaces/IERC1363.sol create mode 100644 src/interfaces/IERC1363Receiver.sol create mode 100644 src/interfaces/IERC1363Spender.sol create mode 100644 src/interfaces/IStakeX.sol create mode 100644 test/BloodX.t.sol delete mode 100644 test/Counter.t.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 09880b1..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: test - -on: workflow_dispatch - -env: - FOUNDRY_PROFILE: ci - -jobs: - check: - strategy: - fail-fast: true - - name: Foundry project - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly - - - name: Run Forge build - run: | - forge --version - forge build --sizes - id: build - - - name: Run Forge tests - run: | - forge test -vvv - id: test diff --git a/.gitmodules b/.gitmodules index ca187d8..1b54971 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/solmate"] path = lib/solmate url = https://github.com/Rari-Capital/solmate +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/foundry.toml b/foundry.toml index 4ff40c4..a2a40da 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,4 +3,6 @@ src = "src" out = "out" libs = ["lib"] -# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file +# Remappings in remappings.txt + +# See more config options https://github.com/gakonst/foundry/tree/master/config \ No newline at end of file diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..932fddf --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 932fddf69a699a9a80fd2396fd1a2ab91cdda123 diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..6fdbafd --- /dev/null +++ b/remappings.txt @@ -0,0 +1 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ diff --git a/src/BloodX.sol b/src/BloodX.sol new file mode 100644 index 0000000..6551f57 --- /dev/null +++ b/src/BloodX.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: UNLICENSED +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/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/ERC1363.sol b/src/ERC1363.sol new file mode 100644 index 0000000..f7d9e8f --- /dev/null +++ b/src/ERC1363.sol @@ -0,0 +1,131 @@ +// 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 new file mode 100644 index 0000000..55dfc41 --- /dev/null +++ b/src/ERC20.sol @@ -0,0 +1,316 @@ +// 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/StakeX.sol b/src/StakeX.sol new file mode 100644 index 0000000..fcceac2 --- /dev/null +++ b/src/StakeX.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +// didn't use solmate here because totalSupply needs override +import "./interfaces/IStakeX.sol"; +import "./ERC1363.sol"; + +contract StakeX is ERC1363, IERC1363Receiver, IStakeX { + + // when ustaking, at least authorizedSupply/minUnstake stake should be claimed + uint256 internal constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + uint256 internal constant MIN_STAKE = 100000; + uint256 internal constant MIN_UNSTAKE_STEP = 5; + uint256 internal constant USTAKE_TIME = 60 * 60 * 72; + + mapping(address => uint256) private unstakeSlot; + address private tokenContract; + uint256 public maxStake; + + constructor( + string memory name, + string memory symbol, + address _tokenContract + ) ERC20(name, symbol) { + tokenContract = _tokenContract; + _mint(_tokenContract, MIN_STAKE * 1 ether); + } + + function getUnstakeSlot(address account) view public returns (uint256 total, uint256 left, uint256 start) { + uint256 raw = unstakeSlot[account]; + start = uint64(raw); + left = uint96(raw >> (64)); + total = uint96(raw >> (96 + 64)); + } + + function dormantSupply() public view override returns(uint256) { + return balanceOf(tokenContract); + } + + function outstandingSupply() public view override returns(uint256) { + return super.totalSupply() - balanceOf(tokenContract); + } + + function authorizedSupply() public view override returns(uint256) { + return super.totalSupply(); + } + + function totalSupply() public view override(ERC20, IERC20) returns(uint256) { + return outstandingSupply(); + } + + 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 == tokenContract) { + // a user has initiated staking + require(data.length <= 32, "The byte array is too long"); + _stake(from, value, uint256(bytes32(data))); + } else { + emit Received(operator, from, value, data); + } + return this.onTransferReceived.selector; + } + + /** + * @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 override { + super._update(from, to, value); + // don't emit transfer event when updating staking pool. + if (from != tokenContract && to != tokenContract) { + emit Transfer(from, to, value); + } + } + + + function unstake(address from, uint256 amount) external { + address spender = _msgSender(); + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (from != spender) { + _spendAllowance(from, spender, amount); + } + + // prevent unstaking tiny amounts + require(amount >= authorizedSupply() / MIN_STAKE); + + _update(from, tokenContract, amount); + + (, uint256 left, ) = getUnstakeSlot(from); + uint256 total = amount + left; + _setUstakeSlot(from, total, total, block.timestamp); + + emit Transfer(from, address(0), amount); + } + + // totalSupply is activeSupply + stakingPool supply of BloodX + function _stake(address account, uint256 amount, uint256 _totalSupply) internal { + uint256 authorizedStake = authorizedSupply(); + require(authorizedStake > 0, "no stake issued yet"); + require(amount > 0, "can not stake 0 amount"); + require(_totalSupply > 0, "no stake issued yet"); + // to avoid arithmetic overflow amount should be < MAX_INT / authorizedStake; + require(amount < MAX_INT / authorizedStake, "arithmetic overflow"); + uint256 newStake = amount * authorizedStake / _totalSupply; + // check stake limits + if (maxStake > 0) { + require(outstandingSupply() + amount <= maxStake, "not enough stake outstanding"); + } + _update(tokenContract, account, newStake); + emit Transfer(address(0), account, newStake); + require(balanceOf(account) >= authorizedStake / MIN_STAKE, "stake too small"); + } + + function _setUstakeSlot(address account, uint256 total, uint256 left, uint256 start) internal { + unstakeSlot[account] = uint64(start) + (left << 64) + (total << (96 + 64)); + } + + function _vestedStake(uint256 total, uint256 left, uint256 start, uint256 _now) internal pure returns (uint256) { + if (_now <= start) { + return 0; + } + // calculate amountVested + // amountVested is amount that can be withdrawn according to time passed + uint256 timePassed = _now - start; + if (timePassed > USTAKE_TIME) { + timePassed = USTAKE_TIME; + } + uint256 amountVested = total * timePassed / USTAKE_TIME; + uint256 amountFrozen = total - amountVested; + if (left <= amountFrozen) { + return 0; + } + return left - amountFrozen; + } + + // executes a powerdown request + function unstakeTick(address account) public { + (uint256 total,uint256 left,uint256 start) = getUnstakeSlot(account); + uint256 amount = _vestedStake(total, left, start, block.timestamp); + + // prevent power down in tiny steps + uint256 minStep = total / MIN_UNSTAKE_STEP; + require(left <= minStep || minStep <= amount); + + left = left - amount; + // handle ustake completed + if (left == 0) { + start = 0; + total = 0; + } + _setUstakeSlot(account, total, left, start); + bytes memory data = new bytes(32); + uint256 overall = authorizedSupply(); + assembly { mstore(add(data, 32), overall) } + _checkOnTransferReceived(address(this), account, tokenContract, amount, data); + } + +} diff --git a/src/interfaces/IERC1363.sol b/src/interfaces/IERC1363.sol new file mode 100644 index 0000000..57ae276 --- /dev/null +++ b/src/interfaces/IERC1363.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363.sol) + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/interfaces//IERC20.sol"; +import "@openzeppelin/contracts/interfaces//IERC165.sol"; + +interface IERC1363 is IERC20, IERC165 { + /* + * Note: the ERC-165 identifier for this interface is 0xb0202a11. + * 0xb0202a11 === + * bytes4(keccak256('transferAndCall(address,uint256)')) ^ + * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^ + * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^ + * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^ + * bytes4(keccak256('approveAndCall(address,uint256)')) ^ + * bytes4(keccak256('approveAndCall(address,uint256,bytes)')) + */ + + /** + * @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver + * @param to address The address which you want to transfer to + * @param value uint256 The amount of tokens to be transferred + * @return true unless throwing + */ + function transferAndCall(address to, uint256 value) external returns (bool); + + /** + * @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver + * @param to address The address which you want to transfer to + * @param value uint256 The amount of tokens to be transferred + * @param data bytes Additional data with no specified format, sent in call to `to` + * @return true unless throwing + */ + function transferAndCall(address to, uint256 value, bytes memory data) external returns (bool); + + /** + * @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver + * @param from address The address which you want to send tokens from + * @param to address The address which you want to transfer to + * @param value uint256 The amount of tokens to be transferred + * @return true unless throwing + */ + function transferFromAndCall(address from, address to, uint256 value) external returns (bool); + + /** + * @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver + * @param from address The address which you want to send tokens from + * @param to address The address which you want to transfer to + * @param value uint256 The amount of tokens to be transferred + * @param data bytes Additional data with no specified format, sent in call to `to` + * @return true unless throwing + */ + function transferFromAndCall(address from, address to, uint256 value, bytes memory data) external returns (bool); + + /** + * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender + * and then call `onApprovalReceived` on spender. + * @param spender address The address which will spend the funds + * @param value uint256 The amount of tokens to be spent + */ + function approveAndCall(address spender, uint256 value) external returns (bool); + + /** + * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender + * and then call `onApprovalReceived` on spender. + * @param spender address The address which will spend the funds + * @param value uint256 The amount of tokens to be spent + * @param data bytes Additional data with no specified format, sent in call to `spender` + */ + function approveAndCall(address spender, uint256 value, bytes memory data) external returns (bool); +} \ No newline at end of file diff --git a/src/interfaces/IERC1363Receiver.sol b/src/interfaces/IERC1363Receiver.sol new file mode 100644 index 0000000..2f5f641 --- /dev/null +++ b/src/interfaces/IERC1363Receiver.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363Receiver.sol) + +pragma solidity ^0.8.0; + +interface IERC1363Receiver { + + event Received(address operator, address from, uint256 value, bytes data); + + /* + * Note: the ERC-165 identifier for this interface is 0x88a7ca5c. + * 0x88a7ca5c === bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)")) + */ + + /** + * @notice Handle the receipt of ERC1363 tokens + * @dev Any ERC1363 smart contract calls this function on the recipient + * after a `transfer` or a `transferFrom`. This function MAY throw to revert and reject the + * transfer. Return of other than the magic value MUST result in the + * transaction being reverted. + * Note: the token contract address is always the message sender. + * @param operator address The address which called `transferAndCall` or `transferFromAndCall` function + * @param from address The address which are token transferred from + * @param value uint256 The amount of tokens transferred + * @param data bytes Additional data with no specified format + * @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` + * unless throwing + */ + function onTransferReceived( + address operator, + address from, + uint256 value, + bytes memory data + ) external returns (bytes4); +} \ No newline at end of file diff --git a/src/interfaces/IERC1363Spender.sol b/src/interfaces/IERC1363Spender.sol new file mode 100644 index 0000000..9df76c5 --- /dev/null +++ b/src/interfaces/IERC1363Spender.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363Spender.sol) + +pragma solidity ^0.8.0; + +interface IERC1363Spender { + /* + * Note: the ERC-165 identifier for this interface is 0x7b04a2d0. + * 0x7b04a2d0 === bytes4(keccak256("onApprovalReceived(address,uint256,bytes)")) + */ + + /** + * @notice Handle the approval of ERC1363 tokens + * @dev Any ERC1363 smart contract calls this function on the recipient + * after an `approve`. This function MAY throw to revert and reject the + * approval. Return of other than the magic value MUST result in the + * transaction being reverted. + * Note: the token contract address is always the message sender. + * @param owner address The address which called `approveAndCall` function + * @param value uint256 The amount of tokens to be spent + * @param data bytes Additional data with no specified format + * @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))` + * unless throwing + */ + function onApprovalReceived(address owner, uint256 value, bytes memory data) external returns (bytes4); +} \ No newline at end of file diff --git a/src/interfaces/IStakeX.sol b/src/interfaces/IStakeX.sol new file mode 100644 index 0000000..5ef2aaa --- /dev/null +++ b/src/interfaces/IStakeX.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +interface IStakeX { + + function dormantSupply() external view returns(uint256); + + function outstandingSupply() external view returns(uint256); + + function authorizedSupply() external view returns(uint256); + +} diff --git a/test/BloodX.t.sol b/test/BloodX.t.sol new file mode 100644 index 0000000..b596bad --- /dev/null +++ b/test/BloodX.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/BloodX.sol"; +import "../src/StakeX.sol"; + +contract BloodXTest is Test { + BloodX public bloodX; + StakeX public stakeX; + uint256 constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + + function setUp() public { + bloodX = new BloodX("name", "SYM"); + stakeX = new StakeX("nameStake", "SYS", address(bloodX)); + bloodX.setStakingContract(address(stakeX)); + } + + function test_MintStakeUnstake(address account, uint256 amount) public { + vm.assume(amount > 1); + vm.assume(amount < MAX_INT / 100000 ether); + vm.assume(account != address(0)); + vm.assume(account != address(bloodX)); + vm.assume(account != address(stakeX)); + + // test mint + uint256 totalSupplyBefore = bloodX.totalSupply(); + uint256 balanceBefore = bloodX.balanceOf(account); + bloodX.purchase(account, amount); + uint256 totalAfter = bloodX.totalSupply(); + assertEq(totalAfter, totalSupplyBefore + amount, "total supply should match"); + assertEq(bloodX.balanceOf(account), balanceBefore + amount, "balance should match"); + + // test stake + uint256 newStake = amount / 2 * 100000 ether / totalAfter; + { + uint256 outstandingBefore = stakeX.totalSupply(); + uint256 stakeBalanceBefore = stakeX.balanceOf(account); + vm.prank(account); + bloodX.stake(account, amount / 2); + assertEq(bloodX.totalSupply(), totalSupplyBefore + (amount - (amount / 2)), "total supply should match after stake"); + assertEq(bloodX.balanceOf(account), balanceBefore + (amount - (amount / 2)), "balance should match after stake"); + assertEq(outstandingBefore + newStake, stakeX.totalSupply(), "outstanding supply should match"); + assertEq(stakeBalanceBefore + newStake, stakeX.balanceOf(account), "balance of stake account should match"); + } + + // test unstake + { + (uint256 totalBefore, uint256 leftBefore,) = stakeX.getUnstakeSlot(account); + vm.prank(account); + stakeX.unstake(account, newStake / 2); + uint256 timeBefore = block.timestamp; + vm.warp(timeBefore + 60 * 60 * 36); + stakeX.unstakeTick(account); + (uint256 total, uint256 left, uint256 start) = stakeX.getUnstakeSlot(account); + assertEq(total, totalBefore + (newStake / 2), "total unstake should match"); + assertApproxEqAbs(left, leftBefore + (newStake / 4), 1); + assertEq(start, timeBefore, "time unstake should match"); + vm.warp(timeBefore + 60 * 60 * 72); + stakeX.unstakeTick(account); + } + } + +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 30235e8..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function testIncrement() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testSetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -}