initial commit
This commit is contained in:
parent
1c1f5b45fe
commit
5711f24e98
16 changed files with 998 additions and 73 deletions
34
.github/workflows/test.yml
vendored
34
.github/workflows/test.yml
vendored
|
|
@ -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
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,4 +3,6 @@ src = "src"
|
|||
out = "out"
|
||||
libs = ["lib"]
|
||||
|
||||
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
|
||||
# Remappings in remappings.txt
|
||||
|
||||
# See more config options https://github.com/gakonst/foundry/tree/master/config
|
||||
1
lib/openzeppelin-contracts
Submodule
1
lib/openzeppelin-contracts
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 932fddf69a699a9a80fd2396fd1a2ab91cdda123
|
||||
1
remappings.txt
Normal file
1
remappings.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
|
||||
159
src/BloodX.sol
Normal file
159
src/BloodX.sol
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
131
src/ERC1363.sol
Normal file
131
src/ERC1363.sol
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
316
src/ERC20.sol
Normal file
316
src/ERC20.sol
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
173
src/StakeX.sol
Normal file
173
src/StakeX.sol
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
73
src/interfaces/IERC1363.sol
Normal file
73
src/interfaces/IERC1363.sol
Normal file
|
|
@ -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);
|
||||
}
|
||||
35
src/interfaces/IERC1363Receiver.sol
Normal file
35
src/interfaces/IERC1363Receiver.sol
Normal file
|
|
@ -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);
|
||||
}
|
||||
26
src/interfaces/IERC1363Spender.sol
Normal file
26
src/interfaces/IERC1363Spender.sol
Normal file
|
|
@ -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);
|
||||
}
|
||||
12
src/interfaces/IStakeX.sol
Normal file
12
src/interfaces/IStakeX.sol
Normal file
|
|
@ -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);
|
||||
|
||||
}
|
||||
65
test/BloodX.t.sol
Normal file
65
test/BloodX.t.sol
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue