DCIPs/assets/eip-3267/contracts/BaseLock.sol

511 lines
22 KiB
Solidity

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.7.1;
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
import { ABDKMath64x64 } from "abdk-libraries-solidity/ABDKMath64x64.sol";
import { ERC1155WithTotals } from "./ERC1155/ERC1155WithTotals.sol";
import { ERC1155Holder } from "@openzeppelin/contracts/token/ERC1155/ERC1155Holder.sol";
import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/ERC721Holder.sol";
/// @title A base class to lock collaterals and distribute them proportional to an oracle result.
/// @author Victor Porton
/// @notice Not audited, not enough tested.
///
/// One can also donate/bequest a smart wallet (explain how).
///
/// We have two kinds of ERC-1155 token IDs:
/// - conditional tokens: numbers < 2**64
/// - a combination of a collateral contract address and collateral token ID
/// (a counter of donated amount of collateral tokens, don't confuse with collateral tokens themselves)
///
/// Inheriting from here don't forget to create `createOracle()` external method.
abstract contract BaseLock is
ERC1155WithTotals,
ERC1155Holder, // You are recommended to use `donate()` function instead.
ERC721Holder // It can be used through an ERC-1155 wrapper.
{
using ABDKMath64x64 for int128;
using SafeMath for uint256;
/// Emitted when an oracle is created.
/// @param oracleId The ID of the created oracle.
event OracleCreated(uint64 oracleId);
/// Emitted when an oracle owner is set.
/// @param oracleOwner Who created an oracle
/// @param oracleId The ID of the oracle.
event OracleOwnerChanged(address indexed oracleOwner, uint64 indexed oracleId);
/// Emitted when an oracle owner is set.
/// @param sender Who created the condition
/// @param customer The owner of the condition.
/// @param condition The created condition ID.
event ConditionCreated(address indexed sender, address indexed customer, uint256 indexed condition);
/// Emitted when a collateral is donated.
/// @param collateralContractAddress The ERC-1155 contract of the donated token.
/// @param collateralTokenId The ERC-1155 ID of the donated token.
/// @param sender Who donated.
/// @param amount The amount donated.
/// @param to Whose account the donation is assigned to.
/// @param data Additional transaction data.
event DonateCollateral(
IERC1155 indexed collateralContractAddress,
uint256 indexed collateralTokenId,
address indexed sender,
uint256 amount,
address to,
bytes data
);
/// Emitted when an oracle is marked as having finished its work.
/// @param oracleId The oracle ID.
event OracleFinished(uint64 indexed oracleId);
/// Emitted when collateral is withdrawn.
/// @param contractAddress The ERC-1155 contract of the collateral token.
/// @param collateralTokenId The ERC-1155 token ID of the collateral.
/// @param oracleId The oracle ID for which withdrawal is done.
/// @param user Who has withdrawn.
/// @param amount The amount withdrawn.
event CollateralWithdrawn(
IERC1155 indexed contractAddress,
uint256 indexed collateralTokenId,
uint64 indexed oracleId,
address user,
uint256 amount
);
// Next ID.
uint64 public maxOracleId; // It doesn't really need to be public.
uint64 public maxConditionId; // It doesn't really need to be public.
// Mapping (oracleId => oracle owner).
mapping(uint64 => address) private oracleOwnersMap;
// Mapping (oracleId => time) the max time for first withdrawal.
mapping(uint64 => uint) private gracePeriodEnds;
// The user lost the right to transfer conditional tokens: (user => (conditionalToken => bool)).
mapping(address => mapping(uint256 => bool)) private userUsedRedeemMap;
// Mapping (token => (original user => amount)) used to calculate withdrawal of collateral amounts.
mapping(uint256 => mapping(address => uint256)) public lastCollateralBalanceFirstRoundMap;
// Mapping (token => (original user => amount)) used to calculate withdrawal of collateral amounts.
mapping(uint256 => mapping(address => uint256)) public lastCollateralBalanceSecondRoundMap;
/// Mapping (oracleId => amount user withdrew in first round) (see `docs/Calculations.md`).
mapping(uint64 => uint256) public usersWithdrewInFirstRound;
// Mapping (condition ID => original account)
mapping(uint256 => address) public conditionOwners;
/// Constructor.
/// @param _uri Our ERC-1155 tokens description URI.
constructor(string memory _uri) ERC1155WithTotals(_uri) {
_registerInterface(
BaseLock(0).onERC1155Received.selector ^
BaseLock(0).onERC1155BatchReceived.selector ^
BaseLock(0).onERC721Received.selector
);
}
/// This function makes no sense, because it would produce a condition with zero tokens.
// function createCondition() public returns (uint64) {
// return _createCondition();
// }
/// Modify the owner of an oracle.
/// @param _newOracleOwner New owner.
/// @param _oracleId The oracle whose owner to change.
function changeOracleOwner(address _newOracleOwner, uint64 _oracleId) public _isOracle(_oracleId) {
oracleOwnersMap[_oracleId] = _newOracleOwner;
emit OracleOwnerChanged(_newOracleOwner, _oracleId);
}
/// Set the end time of the grace period.
///
/// The first withdrawal can be done *only* during the grace period.
/// The second withdrawal can be done after the end of the grace period and only if the first withdrawal was done.
///
/// The intention of the grace period is to check which of users are active ("alive").
function updateGracePeriodEnds(uint64 _oracleId, uint _time) public _isOracle(_oracleId) {
gracePeriodEnds[_oracleId] = _time;
}
/// Donate funds in an ERC-1155 token.
///
/// First, the collateral token need to be approved to be spent by this contract from the address `_from`.
///
/// It also mints a token (with a different ID), that counts donations in that token.
///
/// @param _collateralContractAddress The collateral ERC-1155 contract address.
/// @param _collateralTokenId The collateral ERC-1155 token ID.
/// @param _oracleId The oracle ID to whose ecosystem to donate to.
/// @param _amount The amount to donate.
/// @param _from From whom to take the donation.
/// @param _to On whose account the donation amount is assigned.
/// @param _data Additional transaction data.
function donate(
IERC1155 _collateralContractAddress,
uint256 _collateralTokenId,
uint64 _oracleId,
uint256 _amount,
address _from,
address _to,
bytes calldata _data) public
{
uint _donatedPerOracleCollateralTokenId = _collateralDonatedPerOracleTokenId(_collateralContractAddress, _collateralTokenId, _oracleId);
_mint(_to, _donatedPerOracleCollateralTokenId, _amount, _data);
uint _donatedCollateralTokenId = _collateralDonatedTokenId(_collateralContractAddress, _collateralTokenId);
_mint(_to, _donatedCollateralTokenId, _amount, _data);
emit DonateCollateral(_collateralContractAddress, _collateralTokenId, _from, _amount, _to, _data);
_collateralContractAddress.safeTransferFrom(_from, address(this), _collateralTokenId, _amount, _data); // last against reentrancy attack
}
/// Gather a DeFi profit of a token previous donated to this contact.
/// @param _collateralContractAddress The collateral ERC-1155 contract address.
/// @param _collateralTokenId The collateral ERC-1155 token ID.
/// @param _oracleId The oracle ID to whose ecosystem to donate to.
/// @param _data Additional transaction data.
/// TODO: Batch calls in several tokens and/or to several oracles for less gas usage?
function gatherDeFiProfit(
IERC1155 _collateralContractAddress,
uint256 _collateralTokenId,
uint64 _oracleId,
bytes calldata _data) external
{
uint _donatedPerOracleCollateralTokenId = _collateralDonatedPerOracleTokenId(_collateralContractAddress, _collateralTokenId, _oracleId);
uint _donatedCollateralTokenId = _collateralDonatedTokenId(_collateralContractAddress, _collateralTokenId);
// We consider an overflow an error and just revert:
// FIXME: Impossible due to reentrancy vulnerability? (Really? It's a view!)
uint256 _difference =
_collateralContractAddress.balanceOf(address(this), _collateralTokenId).sub(
balanceOf(address(this), _donatedCollateralTokenId));
uint256 _amount = // rounding down to prevent overflows
_difference *
balanceOf(address(this), _donatedPerOracleCollateralTokenId) /
balanceOf(address(this), _donatedCollateralTokenId);
// Last to avoid reentrancy vulnerability.
donate(
_collateralContractAddress,
_collateralTokenId,
_oracleId,
_amount,
address(this),
address(this),
_data);
}
/// Calculate how much collateral is owed to a user.
/// @param _collateralContractAddress The ERC-1155 collateral token contract.
/// @param _collateralTokenId The ERC-1155 collateral token ID.
/// @param _oracleId From which oracle's "account" to withdraw.
/// @param _condition The condition (the original receiver of a conditional token).
/// @param _user The user to which we may owe.
function collateralOwing(
IERC1155 _collateralContractAddress,
uint256 _collateralTokenId,
uint64 _oracleId,
uint256 _condition,
address _user
) external view returns(uint256) {
bool _inFirstRound = _isInFirstRound(_oracleId);
(, uint256 _donated) =
_collateralOwingBase(_collateralContractAddress, _collateralTokenId, _oracleId, _condition, _user, _inFirstRound);
return _donated;
}
/// Transfer to `msg.sender` the collateral ERC-1155 token.
///
/// The amount transferred is proportional to the score of `_condition` by the oracle.
/// @param _collateralContractAddress The ERC-1155 collateral token contract.
/// @param _collateralTokenId The ERC-1155 collateral token ID.
/// @param _oracleId From which oracle's "account" to withdraw.
/// @param _condition The condition.
/// @param _data Additional data.
///
/// Notes:
/// - It is made impossible to withdraw somebody's other collateral, as otherwise we can't mark non-active
/// accounts in grace period.
/// - We can't transfer to somebody other than `msg.sender` because anybody can transfer
/// (needed for multi-level transfers).
/// - After this function is called, it becomes impossible to transfer the corresponding conditional token
/// of `msg.sender` (to prevent its repeated withdrawal).
function withdrawCollateral(
IERC1155 _collateralContractAddress,
uint256 _collateralTokenId,
uint64 _oracleId,
uint256 _condition,
bytes calldata _data) external
{
require(isOracleFinished(_oracleId), "too early"); // to prevent the denominator or the numerators change meantime
bool _inFirstRound = _isInFirstRound(_oracleId);
userUsedRedeemMap[msg.sender][_condition] = true;
// _burn(msg.sender, _condition, conditionalBalance); // Burning it would break using the same token for multiple markets.
(uint _donatedPerOracleCollateralTokenId, uint256 _owingDonated) =
_collateralOwingBase(_collateralContractAddress, _collateralTokenId, _oracleId, _condition, msg.sender, _inFirstRound);
// Against rounding errors. Not necessary because of rounding down.
// if(_owing > balanceOf(address(this), _collateralTokenId)) _owing = balanceOf(address(this), _collateralTokenId);
if (_owingDonated != 0) {
uint256 _newTotal = totalSupply(_donatedPerOracleCollateralTokenId);
if (_inFirstRound) {
lastCollateralBalanceFirstRoundMap[_donatedPerOracleCollateralTokenId][msg.sender] = _newTotal;
} else {
lastCollateralBalanceSecondRoundMap[_donatedPerOracleCollateralTokenId][msg.sender] = _newTotal;
}
}
if (!_inFirstRound) {
usersWithdrewInFirstRound[_oracleId] = usersWithdrewInFirstRound[_oracleId].add(_owingDonated);
}
// Last to prevent reentrancy attack:
_collateralContractAddress.safeTransferFrom(address(this), msg.sender, _collateralTokenId, _owingDonated, _data);
emit CollateralWithdrawn(
_collateralContractAddress,
_collateralTokenId,
_oracleId,
msg.sender,
_owingDonated
);
}
/// An ERC-1155 function.
///
/// We disallow transfers of conditional tokens after redeem `_to` prevent "gathering" them before redeeming
/// each oracle.
function safeTransferFrom(
address _from,
address _to,
uint256 _id,
uint256 _value,
bytes calldata _data
)
public override
{
_checkTransferAllowed(_id, _from);
_baseSafeTransferFrom(_from, _to, _id, _value, _data);
}
/// An ERC-1155 function.
///
/// We disallow transfers of conditional tokens after redeem `_to` prevent "gathering" them before redeeming
/// each oracle.
function safeBatchTransferFrom(
address _from,
address _to,
uint256[] calldata _ids,
uint256[] calldata _values,
bytes calldata _data
)
public override
{
for(uint _i = 0; _i < _ids.length; ++_i) {
_checkTransferAllowed(_ids[_i], _from);
}
_baseSafeBatchTransferFrom(_from, _to, _ids, _values, _data);
}
// Getters //
/// Get the oracle owner.
/// @param _oracleId The oracle ID.
function oracleOwner(uint64 _oracleId) public view returns (address) {
return oracleOwnersMap[_oracleId];
}
/// Is the oracle marked as having finished its work?
///
/// `oracleId` is the oracle ID.
function isOracleFinished(uint64 /*oracleId*/) public virtual view returns (bool) {
return true;
}
/// Are transfers of a conditinal token locked?
///
/// This is used to prevent its repeated withdrawal.
/// @param _user Querying if locked for this user.
/// @param _condition The condition (the original receiver of a conditional token).
function isConditionalLocked(address _user, uint256 _condition) public view returns (bool) {
return userUsedRedeemMap[_user][_condition];
}
/// Retrieve the end of the grace period.
/// @param _oracleId For which oracle.
function gracePeriodEnd(uint64 _oracleId) public view returns (uint) {
return gracePeriodEnds[_oracleId];
}
// Virtual functions //
/// Current address of a user.
/// @param _originalAddress The original address of the user.
function originalToCurrentAddress(address _originalAddress) internal virtual returns (address) {
return _originalAddress;
}
/// Mint a conditional to a customer.
function _mintToCustomer(address _customer, uint256 _condition, uint256 _amount, bytes calldata _data)
internal virtual
{
require(conditionOwners[_condition] == _customer, "Other's salary get attempt.");
_mint(originalToCurrentAddress(_customer), _condition, _amount, _data);
}
/// Calculate the share of a condition in an oracle's market.
/// @param _oracleId The oracle ID.
/// @return Uses `ABDKMath64x64` number ID.
function _calcRewardShare(uint64 _oracleId, uint256 _condition) internal virtual view returns (int128);
function _calcMultiplier(uint64 _oracleId, uint256 _condition, int128 _oracleShare) internal virtual view returns (int128) {
int128 _rewardShare = _calcRewardShare(_oracleId, _condition);
return _oracleShare.mul(_rewardShare);
}
function _doTransfer(uint256 _id, address _from, address _to, uint256 _value) internal virtual {
_balances[_id][_from] = _balances[_id][_from].sub(_value);
_balances[_id][_to] = _value.add(_balances[_id][_to]);
}
// Internal //
/// Generate the ERC-1155 token ID that counts amount of donations per oracle for a ERC-1155 collateral token.
/// @param _collateralContractAddress The ERC-1155 contract of the collateral token.
/// @param _collateralTokenId The ERC-1155 ID of the collateral token.
/// @param _oracleId The oracle ID.
/// Note: It does not conflict with other tokens kinds, because the only other one is the uint64 conditional.
function _collateralDonatedPerOracleTokenId(IERC1155 _collateralContractAddress, uint256 _collateralTokenId, uint64 _oracleId)
internal pure returns (uint256)
{
return uint256(keccak256(abi.encodePacked(_collateralContractAddress, _collateralTokenId, _oracleId)));
}
/// Generate the ERC-1155 token ID that counts amount of donations for a ERC-1155 collateral token.
/// @param _collateralContractAddress The ERC-1155 contract of the collateral token.
/// @param _collateralTokenId The ERC-1155 ID of the collateral token.
/// Note: It does not conflict with other tokens kinds, because the only other one is the uint64 conditional.
function _collateralDonatedTokenId(IERC1155 _collateralContractAddress, uint256 _collateralTokenId)
internal pure returns (uint256)
{
return uint256(keccak256(abi.encodePacked(_collateralContractAddress, _collateralTokenId)));
}
function _checkTransferAllowed(uint256 _id, address _from) internal view {
require(!userUsedRedeemMap[_from][_id], "You can't trade conditional tokens after redeem.");
}
function _baseSafeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes memory _data) private {
require(_to != address(0), "ERC1155: target address must be non-zero");
require(
_from == msg.sender || _operatorApprovals[_from][msg.sender] == true,
"ERC1155: need operator approval for 3rd party transfers."
);
_doTransfer(_id, _from, _to, _value);
emit TransferSingle(msg.sender, _from, _to, _id, _value);
_doSafeTransferAcceptanceCheck(msg.sender, _from, _to, _id, _value, _data);
}
function _baseSafeBatchTransferFrom(
address _from,
address _to,
uint256[] memory _ids,
uint256[] memory _values,
bytes memory _data
)
private
{
require(_ids.length == _values.length, "ERC1155: IDs and _values must have same lengths");
require(_to != address(0), "ERC1155: target address must be non-zero");
require(
_from == msg.sender || _operatorApprovals[_from][msg.sender] == true,
"ERC1155: need operator approval for 3rd party transfers."
);
for (uint256 _i = 0; _i < _ids.length; ++_i) {
uint256 _id = _ids[_i];
uint256 _value = _values[_i];
_doTransfer(_id, _from, _to, _value);
}
emit TransferBatch(msg.sender, _from, _to, _ids, _values);
_doSafeBatchTransferAcceptanceCheck(msg.sender, _from, _to, _ids, _values, _data);
}
function _createOracle() internal returns (uint64) {
uint64 _oracleId = ++maxOracleId;
oracleOwnersMap[_oracleId] = msg.sender;
emit OracleCreated(_oracleId);
emit OracleOwnerChanged(msg.sender, _oracleId);
return _oracleId;
}
/// Start with 1, not 0, to avoid glitch with `conditionalTokens` variable.
///
/// TODO: Use uint64 variables instead?
function _createCondition(address _customer) internal returns (uint256) {
return _doCreateCondition(_customer);
}
/// Start with 1, not 0, to avoid glitch with `conditionalTokens` variable.
///
/// TODO: Use uint64 variables instead?
function _doCreateCondition(address _customer) internal virtual returns (uint256) {
uint64 _condition = ++maxConditionId;
conditionOwners[_condition] = _customer;
emit ConditionCreated(msg.sender, _customer, _condition);
return _condition;
}
function _collateralOwingBase(
IERC1155 _collateralContractAddress,
uint256 _collateralTokenId,
uint64 _oracleId,
uint256 _condition,
address _user,
bool _inFirstRound
)
private view returns (uint _donatedPerOracleCollateralTokenId, uint256 _donated)
{
uint256 _conditionalBalance = balanceOf(_user, _condition);
uint256 _totalConditionalBalance =
_inFirstRound ? totalSupply(_condition) : usersWithdrewInFirstRound[_oracleId];
_donatedPerOracleCollateralTokenId = _collateralDonatedPerOracleTokenId(_collateralContractAddress, _collateralTokenId, _oracleId);
// Rounded to below for no out-of-funds:
int128 _oracleShare = ABDKMath64x64.divu(_conditionalBalance, _totalConditionalBalance);
uint256 _newDividendsDonated =
totalSupply(_donatedPerOracleCollateralTokenId) -
(_inFirstRound
? lastCollateralBalanceFirstRoundMap[_donatedPerOracleCollateralTokenId][_user]
: lastCollateralBalanceSecondRoundMap[_donatedPerOracleCollateralTokenId][_user]);
int128 _multiplier = _calcMultiplier(_oracleId, _condition, _oracleShare);
_donated = _multiplier.mulu(_newDividendsDonated);
}
function _isInFirstRound(uint64 _oracleId) internal view returns (bool) {
return block.timestamp <= gracePeriodEnds[_oracleId];
}
function _isConditional(uint256 _tokenId) internal pure returns (bool) {
// Zero 2**-192 probability that tokenId < (1<<64) if it's not a conditional.
// Note to auditor: It's a hack, check for no errors carefully.
return _tokenId < (1<<64);
}
modifier _isOracle(uint64 _oracleId) {
require(oracleOwnersMap[_oracleId] == msg.sender, "Not the oracle owner.");
_;
}
modifier checkIsConditional(uint256 _tokenId) {
require(_isConditional(_tokenId), "It's not your conditional.");
_;
}
}