240 lines
12 KiB
Solidity
240 lines
12 KiB
Solidity
|
// SPDX-License-Identifier: CC0-1.0
|
||
|
pragma solidity ^0.7.1;
|
||
|
import "./BaseBidOnAddresses.sol";
|
||
|
|
||
|
/// @title Base class for a "salary" that is paid one token per second using minted conditionals.
|
||
|
/// @author Victor Porton
|
||
|
/// @notice Not audited, not enough tested.
|
||
|
/// It was considered to allow the DAO to adjust registration date to pay salary retrospectively,
|
||
|
/// but this seems giving too much rights to the DAO similarly as if it had the right to declare anyone dead.
|
||
|
///
|
||
|
/// It would cause this effect: A scientist who is already great may register then his date is moved back
|
||
|
/// in time and instantly he or she receives a very big sum of money to his account.
|
||
|
/// If it is done erroneously, there may be no way to move the registration date again forward in time,
|
||
|
/// because the tokens may be already withdrawn. And it cannot be done in a fully decentralized way because
|
||
|
/// it needs oracles. So errors are seem inevitable.
|
||
|
/// On the other hand, somebody malicious may create and register in my system a pool of Ethereum addresses that
|
||
|
/// individuals can receive from them as if they themselves registered in the past.
|
||
|
/// So it in some cases (if the registration date is past the contract deployment) this issue is impossible to
|
||
|
/// mitigate.
|
||
|
///
|
||
|
/// The salary is paid in minted tokens groups into "chains":
|
||
|
/// the original salary token and anyone can replace it by another token, next in the chain.
|
||
|
contract BaseSalary is BaseBidOnAddresses {
|
||
|
/// Salary receiver registered.
|
||
|
/// @param customer The customer address.
|
||
|
/// @param oracleId The oracle ID for which he registers.
|
||
|
/// @param data Additional data.
|
||
|
event CustomerRegistered(
|
||
|
address indexed customer,
|
||
|
uint64 indexed oracleId,
|
||
|
uint256 indexed condition,
|
||
|
bytes data
|
||
|
);
|
||
|
|
||
|
/// Salary tokens minted.
|
||
|
/// @param customer The customer address.
|
||
|
/// @param oracleId The oracle ID.
|
||
|
/// @param amount The minted amount.
|
||
|
/// @param data Additional data.
|
||
|
event SalaryMinted(
|
||
|
address indexed customer,
|
||
|
uint64 indexed oracleId,
|
||
|
uint256 indexed condition,
|
||
|
uint256 amount,
|
||
|
bytes data
|
||
|
);
|
||
|
|
||
|
/// Salary token recreated (salary recalculation request).
|
||
|
/// @param customer The customer address.
|
||
|
/// @param originalCondition The original token ID.
|
||
|
/// @param newCondition The new token ID.
|
||
|
event ConditionReCreate(
|
||
|
address indexed customer,
|
||
|
uint256 indexed originalCondition,
|
||
|
uint256 indexed newCondition
|
||
|
);
|
||
|
|
||
|
// Mapping (condition ID => registration time).
|
||
|
mapping(uint256 => uint) public conditionCreationDates;
|
||
|
// Mapping (condition ID => salary block time).
|
||
|
mapping(uint256 => uint) public lastSalaryDates;
|
||
|
/// Mapping (condition ID => account) - salary recipients.
|
||
|
mapping(uint256 => address) public salaryReceivers;
|
||
|
|
||
|
/// Mapping (condition ID => first condition ID in the chain)
|
||
|
///
|
||
|
/// I call _chain_ of conditions the list of conditions resulting from creating and recreating conditions.
|
||
|
mapping(uint256 => uint256) public firstConditionInChain;
|
||
|
/// Mapping (first condition ID in the chain => last condition ID in the chain)
|
||
|
///
|
||
|
/// I call _chain_ of conditions the list of conditions resulting from creating and recreating conditions.
|
||
|
mapping(uint256 => uint256) public firstToLastConditionInChain;
|
||
|
|
||
|
/// Constructor.
|
||
|
/// @param _uri The ERC-1155 token URI.
|
||
|
constructor(string memory _uri) BaseBidOnAddresses(_uri) { }
|
||
|
|
||
|
/// Mint a salary token.
|
||
|
/// @param _oracleId The oracle ID.
|
||
|
/// @param _condition The condition ID.
|
||
|
/// @param _data Additional data.
|
||
|
/// This method can be called only by the salary receiver.
|
||
|
function mintSalary(uint64 _oracleId, uint256 _condition, bytes calldata _data)
|
||
|
ensureLastConditionInChain(_condition) external
|
||
|
{
|
||
|
uint _lastSalaryDate = lastSalaryDates[_condition];
|
||
|
require(_lastSalaryDate != 0, "You are not registered.");
|
||
|
// Note: Even if you withdraw once per 20 years, you will get only 630,720,000 tokens.
|
||
|
// This number is probably not to big to be displayed well in UIs.
|
||
|
uint256 _amount = (block.timestamp - _lastSalaryDate) * 10**18; // one token per second
|
||
|
_mintToCustomer(msg.sender, firstToLastConditionInChain[_condition], _amount, _data);
|
||
|
lastSalaryDates[_condition] = block.timestamp;
|
||
|
emit SalaryMinted(msg.sender, _oracleId, _condition, _amount, _data);
|
||
|
}
|
||
|
|
||
|
/// Make a new condition that replaces the old one.
|
||
|
///
|
||
|
/// In other words, it is a request to recalculate somebody's salary.
|
||
|
///
|
||
|
/// Anyone can request to recalculate anyone's salary.
|
||
|
///
|
||
|
/// It's also useful to punish someone for decreasing his work performance or an evil act.
|
||
|
/// This is to be called among other when a person dies.
|
||
|
///
|
||
|
/// Recalculation is also forced when a salary receiver transfers away his current salary token.
|
||
|
/// It is useful to remove a trader's incentive to kill the issuer to reduce the circulating supply.
|
||
|
///
|
||
|
/// Issue to solve later: Should we recommend:
|
||
|
/// - calling this function on each new project milestone?
|
||
|
/// - calling this function regularly (e.g. every week)?
|
||
|
///
|
||
|
/// This function also withdraws the old token.
|
||
|
function recreateCondition(uint256 _condition) public returns (uint256) {
|
||
|
return _recreateCondition(_condition);
|
||
|
}
|
||
|
|
||
|
function _doCreateCondition(address _customer) internal virtual override returns (uint256) {
|
||
|
uint256 _condition = super._doCreateCondition(_customer);
|
||
|
salaryReceivers[_condition] = _customer;
|
||
|
conditionCreationDates[_condition] = block.timestamp;
|
||
|
firstConditionInChain[_condition] = _condition;
|
||
|
firstToLastConditionInChain[_condition] = _condition;
|
||
|
return _condition;
|
||
|
}
|
||
|
|
||
|
/// Make a new condition that replaces the old one.
|
||
|
/// The same can be done by transferring to yourself 0 tokens, but this method uses less gas.
|
||
|
///
|
||
|
/// We need to create a new condition every time when an outgoimg transfer of a conditional token happens.
|
||
|
/// Otherwise an investor would gain if he kills a scientist to reduce the circulating supply of his token to increase the price.
|
||
|
/// Allowing old tokens to be exchangeable for new ones? (Allowing the reverse swap would create killer's gain.)
|
||
|
/// Additional benefit of this solution: We can have different rewards at different stages of project,
|
||
|
/// what may be beneficial for early startups funding.
|
||
|
///
|
||
|
/// Problem to be solved later: There should be an advice to switch to a new token at each milestone of a project?
|
||
|
///
|
||
|
/// Anyone can create a ERC-1155 contract that allows to use any of the tokens in the list
|
||
|
/// by locking any of the tokens in the list as a new "general" token. We should recommend customers not to
|
||
|
/// use such contracts, because it creates for them the killer exploit.
|
||
|
///
|
||
|
/// If we would exchange the old and new tokens for the same amounts of collaterals, then it would be
|
||
|
/// effectively the same token and therefore minting more new token would possibly devalue the old one,
|
||
|
/// thus triggering the killer's exploit again. So we make old and new completely independent.
|
||
|
///
|
||
|
/// Old token is 1:1 converted to the new token.
|
||
|
///
|
||
|
/// Remark: To make easy to exchange the token even if it is recreated, we can make a wrapper or locker
|
||
|
/// token that uses `firstConditionInChain[]` to aggregate several tokens together.
|
||
|
/// A similar wrapper (the customer need to `setApprovalForAll()` on it) that uses
|
||
|
/// `firstToLastConditionInChain[]` can be used to transfer away recreated tokens
|
||
|
/// even if an evil DAO tries to frontrun the customer by recreating his tokens very often.
|
||
|
/// TODO: Test that it's possible to create such a locker.
|
||
|
///
|
||
|
/// Note: That wrapper could be carelessly used to create the investor's killing customer incentive
|
||
|
/// by the customer using it to transfer to an investor. Even if the customer uses it only for
|
||
|
/// exchanges, an investor can buy at an exchange and be a killer.
|
||
|
/// To make it safe, it must stop accepting any new tokens after a transfer.
|
||
|
/// It can determine if a token is new just comparing by `<` operator.
|
||
|
/// It's strongly recommended that an app that uses this contract provides its own swap/exchange UI
|
||
|
/// and warns the user not to use arbitrary exchanges as being an incentive to kill the user.
|
||
|
///
|
||
|
/// We allow anybody (not just the account owner or DAO) to recreate a condition, because:
|
||
|
/// - Exchanges can create a "composite" token that allows to withdraw any of the tokens in the chain
|
||
|
/// up to a certain period of time (using on-chain `conditionCreationDates`).
|
||
|
/// - Therefore somebody's token can be withdrawn even if its ID changes arbitrarily often.
|
||
|
///
|
||
|
/// @param _condition The condition ID.
|
||
|
function _recreateCondition(uint256 _condition)
|
||
|
internal ensureFirstConditionInChain(_condition) returns (uint256)
|
||
|
{
|
||
|
address _customer = salaryReceivers[_condition];
|
||
|
uint256 _oldCondition = firstToLastConditionInChain[_condition];
|
||
|
uint256 _newCondition = _doCreateCondition(_customer);
|
||
|
firstConditionInChain[_newCondition] = _condition;
|
||
|
|
||
|
uint256 _amount = _balances[_oldCondition][_customer];
|
||
|
_balances[_newCondition][_customer] = _amount;
|
||
|
_balances[_oldCondition][_customer] = 0;
|
||
|
|
||
|
// TODO: Should we swap two following lines?
|
||
|
emit TransferSingle(msg.sender, _customer, address(0), _condition, _amount);
|
||
|
emit TransferSingle(msg.sender, address(0), _customer, _newCondition, _amount);
|
||
|
|
||
|
lastSalaryDates[_newCondition] = lastSalaryDates[_condition];
|
||
|
// TODO: Should we here set `lastSalaryDates[_condition] = 0` to save storage space? // TODO: It would also eliminate the need to check in mint function.
|
||
|
|
||
|
emit ConditionReCreate(_customer, _condition, _newCondition);
|
||
|
return _newCondition;
|
||
|
}
|
||
|
|
||
|
/// Check if it is the first condition in a chain of conditions.
|
||
|
/// @param _id The condition ID.
|
||
|
///
|
||
|
/// Must be called with `_id != 0`.
|
||
|
function isFirstConditionInChain(uint256 _id) internal view returns (bool) {
|
||
|
return firstConditionInChain[_id] == _id;
|
||
|
}
|
||
|
|
||
|
/// Check if it is the last condition in a chain of conditions.
|
||
|
/// @param _id The condition ID.
|
||
|
///
|
||
|
/// Must be called with `_id != 0`.
|
||
|
///
|
||
|
/// TODO: Should make this function public?
|
||
|
function isLastConditionInChain(uint256 _id) internal view returns (bool) {
|
||
|
return firstToLastConditionInChain[firstConditionInChain[_id]] == _id;
|
||
|
}
|
||
|
|
||
|
function _doTransfer(uint256 _id, address _from, address _to, uint256 _value) internal virtual override {
|
||
|
super._doTransfer(_id, _from, _to, _value);
|
||
|
|
||
|
if (_id != 0 && salaryReceivers[_id] == msg.sender) {
|
||
|
if (isLastConditionInChain(_id)) { // correct because `_id != 0`
|
||
|
_recreateCondition(_id);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function _registerCustomer(address _customer, uint64 _oracleId, bytes calldata _data)
|
||
|
virtual internal returns (uint256)
|
||
|
{
|
||
|
uint256 _condition = _doCreateCondition(_customer);
|
||
|
lastSalaryDates[_condition] = block.timestamp;
|
||
|
emit CustomerRegistered(msg.sender, _oracleId, _condition, _data);
|
||
|
return _condition;
|
||
|
}
|
||
|
|
||
|
modifier ensureFirstConditionInChain(uint256 _id) {
|
||
|
// TODO: Is `_id != 0` check needed?
|
||
|
require(_isConditional(_id) && _id != 0 && isFirstConditionInChain(_id), "Only for the last salary token.");
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier ensureLastConditionInChain(uint256 _id) {
|
||
|
// TODO: Is `_id != 0` check needed?
|
||
|
require(_isConditional(_id) && _id != 0 && isLastConditionInChain(_id), "Only for the last salary token.");
|
||
|
_;
|
||
|
}
|
||
|
}
|