// 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."); _; } }