// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract TwoWeeksNotice {
struct StakeState {
uint64 balance;
uint64 unlockPeriod; // time it takes from requesting withdraw to being able to withdraw
uint64 lockedUntil; // 0 if withdraw is not requested
uint64 since;
uint128 accumulated; // token-days staked
uint128 accumulatedStrict; // token-days staked sans withdraw periods
}
event StakeUpdate(address indexed from, uint64 balance);
event WithdrawRequest(address indexed from, uint64 until);
mapping(address => StakeState) private _states;
IERC20 private token;
constructor (IERC20 _token) public {
token = _token;
}
function getStakeState(address account) external view returns (uint64, uint64, uint64, uint64) {
StakeState storage ss = _states[account];
return (ss.balance, ss.unlockPeriod, ss.lockedUntil, ss.since);
}
function getAccumulated(address account) external view returns (uint128, uint128) {
StakeState storage ss = _states[account];
return (ss.accumulated, ss.accumulatedStrict);
}
function estimateAccumulated(address account) external view returns (uint128, uint128) {
StakeState storage ss = _states[account];
uint128 sum = ss.accumulated;
uint128 sumStrict = ss.accumulatedStrict;
if (ss.balance > 0) {
uint256 until = block.timestamp;
if (ss.lockedUntil > 0 && ss.lockedUntil < block.timestamp) {
until = ss.lockedUntil;
}
if (until > ss.since) {
uint128 delta = uint128( (uint256(ss.balance) * (until - ss.since))/86400 );
sum += delta;
if (ss.lockedUntil == 0) {
sumStrict += delta;
}
}
}
return (sum, sumStrict);
}
function updateAccumulated(StakeState storage ss) private {
if (ss.balance > 0) {
uint256 until = block.timestamp;
if (ss.lockedUntil > 0 && ss.lockedUntil < block.timestamp) {
until = ss.lockedUntil;
}
if (until > ss.since) {
uint128 delta = uint128( (uint256(ss.balance) * (until - ss.since))/86400 );
ss.accumulated += delta;
if (ss.lockedUntil == 0) {
ss.accumulatedStrict += delta;
}
}
}
}
function stake(uint64 amount, uint64 unlockPeriod) external {
StakeState storage ss = _states[msg.sender];
require(amount > 0, "amount must be positive");
require(ss.balance <= amount, "cannot decrease balance");
require(unlockPeriod <= 1000 days, "unlockPeriod cannot be higher than 1000 days");
require(ss.unlockPeriod <= unlockPeriod, "cannot decrease unlock period");
require(unlockPeriod >= 2 weeks, "unlock period can't be less than 2 weeks");
updateAccumulated(ss);
uint128 delta = amount - ss.balance;
if (delta > 0) {
require(token.transferFrom(msg.sender, address(this), delta), "transfer unsuccessful");
}
ss.balance = amount;
ss.unlockPeriod = unlockPeriod;
ss.lockedUntil = 0;
ss.since = uint64(block.timestamp);
emit StakeUpdate(msg.sender, amount);
}
function requestWithdraw() external {
StakeState storage ss = _states[msg.sender];
require(ss.balance > 0);
updateAccumulated(ss);
ss.since = uint64(block.timestamp);
ss.lockedUntil = uint64(block.timestamp + ss.unlockPeriod);
}
function withdraw(address to) external {
StakeState storage ss = _states[msg.sender];
require(ss.balance > 0, "must have tokens to withdraw");
require(ss.lockedUntil != 0, "unlock not requested");
require(ss.lockedUntil < block.timestamp, "still locked");
updateAccumulated(ss);
uint128 balance = ss.balance;
ss.balance = 0;
ss.unlockPeriod = 0;
ss.lockedUntil = 0;
ss.since = 0;
require(token.transfer(to, balance), "transfer unsuccessful");
emit StakeUpdate(msg.sender, 0);
}
}
{
"compilationTarget": {
"TwoWeeksNotice.sol": "TwoWeeksNotice"
},
"evmVersion": "istanbul",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"contract IERC20","name":"_token","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":false,"internalType":"uint64","name":"balance","type":"uint64"}],"name":"StakeUpdate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":false,"internalType":"uint64","name":"until","type":"uint64"}],"name":"WithdrawRequest","type":"event"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"estimateAccumulated","outputs":[{"internalType":"uint128","name":"","type":"uint128"},{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"getAccumulated","outputs":[{"internalType":"uint128","name":"","type":"uint128"},{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"getStakeState","outputs":[{"internalType":"uint64","name":"","type":"uint64"},{"internalType":"uint64","name":"","type":"uint64"},{"internalType":"uint64","name":"","type":"uint64"},{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"requestWithdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint64","name":"amount","type":"uint64"},{"internalType":"uint64","name":"unlockPeriod","type":"uint64"}],"name":"stake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]