diff --git a/contracts/SuperNFT.sol b/contracts/SuperNFT.sol new file mode 100644 index 0000000..d5b18fc --- /dev/null +++ b/contracts/SuperNFT.sol @@ -0,0 +1,48 @@ +pragma solidity ^0.6.2; + +// REMIX +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/token/ERC721/ERC721.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/token/ERC20/ERC20.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/utils/Counters.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/utils/EnumerableSet.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/math/SafeMath.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/utils/Address.sol"; + +// TRUFFLE +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; +import "@openzeppelin/contracts/utils/EnumerableSet.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +// SuperNFT SMART CONTRACT +contract SuperNFT is ERC721 { + using EnumerableSet for EnumerableSet.UintSet; + using Counters for Counters.Counter; + Counters.Counter private _tokenIds; + mapping(string => uint8) hashes; + + /** + * Mint + Issue NFT + * + * @param recipient - NFT will be issued to recipient + * @param hash - Artwork IPFS hash + * @param data - Artwork URI/Data + */ + function issueToken( + address recipient, + string memory hash, + string memory data + ) public returns (uint256) { + require(hashes[hash] != 1); + hashes[hash] = 1; + _tokenIds.increment(); + uint256 newTokenId = _tokenIds.current(); + _mint(recipient, newTokenId); + _setTokenURI(newTokenId, data); + return newTokenId; + } + + constructor() public ERC721("SUPER NFT", "SNFT") {} +} diff --git a/contracts/SuperNFTVestCliff.sol b/contracts/SuperNFTVestCliff.sol new file mode 100644 index 0000000..512fda3 --- /dev/null +++ b/contracts/SuperNFTVestCliff.sol @@ -0,0 +1,246 @@ +pragma solidity ^0.6.2; + +// REMIX +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/token/ERC20/ERC20.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/token/ERC721/ERC721.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/math/SafeMath.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/utils/Address.sol"; + +// TRUFFLE +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +contract SuperNFTVestCliff { + using SafeMath for uint256; + using Address for address; + + address public tokenAddress; + address public nftAddress; + + event Claimed( + address owner, + uint256 nftId, + address beneficiary, + uint256 amount, + uint256 index + ); + event ClaimCreated( + address owner, + uint256 nftId, + uint256 totalAmount, + uint256 index + ); + + struct Claim { + address owner; + uint256 nftId; + uint256[] timePeriods; + uint256[] tokenAmounts; + uint256 totalAmount; + uint256 amountClaimed; + uint256 periodsClaimed; + } + Claim[] private claims; + + mapping(address => uint256[]) private _ownerClaims; + mapping(uint256 => uint256[]) private _nftClaims; + + constructor(address _tokenAddress, address _nftAddress) public { + require( + _tokenAddress.isContract(), + "_tokenAddress must be a contract address" + ); + require( + _nftAddress.isContract(), + "_nftAddress must be a contract address" + ); + tokenAddress = _tokenAddress; + nftAddress = _nftAddress; + } + + /** + * Get Owner Claims + * + * @param owner - Claim Owner Address + */ + function ownerClaims(address owner) + external + view + returns (uint256[] memory) + { + require(owner != address(0), "Owner address cannot be 0"); + return _ownerClaims[owner]; + } + + /** + * Get NFT Claims + * + * @param nftId - NFT ID + */ + function nftClaims(uint256 nftId) external view returns (uint256[] memory) { + require(nftId != 0, "nftId cannot be 0"); + return _nftClaims[nftId]; + } + + /** + * Get Amount Claimed + * + * @param index - Claim Index + */ + function claimed(uint256 index) external view returns (uint256) { + return claims[index].amountClaimed; + } + + /** + * Get Total Claim Amount + * + * @param index - Claim Index + */ + function totalAmount(uint256 index) external view returns (uint256) { + return claims[index].totalAmount; + } + + /** + * Get Time Periods of Claim + * + * @param index - Claim Index + */ + function timePeriods(uint256 index) + external + view + returns (uint256[] memory) + { + return claims[index].timePeriods; + } + + /** + * Get Token Amounts of Claim + * + * @param index - Claim Index + */ + function tokenAmounts(uint256 index) + external + view + returns (uint256[] memory) + { + return claims[index].tokenAmounts; + } + + /** + * Create a Claim - To Vest Tokens to NFT + * + * @param _nftId - Tokens will be claimed by owner of _nftId + * @param _timePeriods - uint256 Array of Epochs + * @param _tokenAmounts - uin256 Array of Amounts to transfer at each time period + */ + function createClaim( + uint256 _nftId, + uint256[] memory _timePeriods, + uint256[] memory _tokenAmounts + ) public returns (bool) { + require(_nftId != 0, "Cannot Vest to NFT 0"); + require( + _timePeriods.length == _tokenAmounts.length, + "_timePeriods & _tokenAmounts length mismatch" + ); + // Calculate total amount + uint256 _totalAmount = 0; + for (uint256 i = 0; i < _tokenAmounts.length; i++) { + _totalAmount = _totalAmount.add(_tokenAmounts[i]); + } + require(_totalAmount > 0, "Provide Token Amounts to Vest"); + require( + ERC20(tokenAddress).allowance(msg.sender, address(this)) >= + _totalAmount, + "Provide token allowance to SuperStreamClaim contract" + ); + // Transfer Tokens to SuperStreamClaim + ERC20(tokenAddress).transferFrom( + msg.sender, + address(this), + _totalAmount + ); + // Create Claim + Claim memory claim = + Claim({ + owner: msg.sender, + nftId: _nftId, + timePeriods: _timePeriods, + tokenAmounts: _tokenAmounts, + totalAmount: _totalAmount, + amountClaimed: 0, + periodsClaimed: 0 + }); + claims.push(claim); + uint256 index = claims.length - 1; + // Map Claim Index to Owner & Beneficiary + _ownerClaims[msg.sender].push(index); + _nftClaims[_nftId].push(index); + emit ClaimCreated(msg.sender, _nftId, _totalAmount, index); + return true; + } + + /** + * Claim Tokens + * + * @param index - Index of the Claim + */ + function claim(uint256 index) external { + Claim storage claim = claims[index]; + // Check if msg.sender is the owner of the NFT + require( + msg.sender == ERC721(nftAddress).ownerOf(claim.nftId), + "msg.sender must own the NFT" + ); + // Check if anything is left to release + require( + claim.periodsClaimed < claim.timePeriods.length, + "Nothing to release" + ); + // Calculate releasable amount + uint256 amount = 0; + for ( + uint256 i = claim.periodsClaimed; + i < claim.timePeriods.length; + i++ + ) { + if (claim.timePeriods[i] <= block.timestamp) { + amount = amount.add(claim.tokenAmounts[i]); + claim.periodsClaimed = claim.periodsClaimed.add(1); + } else { + break; + } + } + // If there is any amount to release + require(amount > 0, "Nothing to release"); + // Transfer Tokens from Owner to Beneficiary + ERC20(tokenAddress).transfer(msg.sender, amount); + claim.amountClaimed = claim.amountClaimed.add(amount); + emit Claimed(claim.owner, claim.nftId, msg.sender, amount, index); + } + + /** + * Get Amount of tokens that can be claimed + * + * @param index - Index of the Claim + */ + function claimableAmount(uint256 index) public view returns (uint256) { + Claim storage claim = claims[index]; + // Calculate Claimable Amount + uint256 amount = 0; + for ( + uint256 i = claim.periodsClaimed; + i < claim.timePeriods.length; + i++ + ) { + if (claim.timePeriods[i] <= block.timestamp) { + amount = amount.add(claim.tokenAmounts[i]); + } else { + break; + } + } + return amount; + } +} diff --git a/contracts/SuperNFTVestStream.sol b/contracts/SuperNFTVestStream.sol new file mode 100644 index 0000000..0f14003 --- /dev/null +++ b/contracts/SuperNFTVestStream.sol @@ -0,0 +1,272 @@ +pragma solidity ^0.6.2; + +// REMIX +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/token/ERC20/ERC20.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/token/ERC721/ERC721.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/math/SafeMath.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/utils/Address.sol"; + +// TRUFFLE +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +contract SuperNFTVestStream { + using SafeMath for uint256; + using Address for address; + + address public tokenAddress; + address public nftAddress; + + event Claimed( + address owner, + uint256 nftId, + address beneficiary, + uint256 amount, + uint256 index + ); + event ClaimCreated( + address owner, + uint256 nftId, + uint256 totalAmount, + uint256 index + ); + + struct Claim { + address owner; + uint256 nftId; + uint256[] timePeriods; + uint256[] tokenAmounts; + uint256 totalAmount; + uint256 amountClaimed; + uint256 periodsClaimed; + } + Claim[] private claims; + + mapping(address => uint256[]) private _ownerClaims; + mapping(uint256 => uint256[]) private _nftClaims; + + constructor(address _tokenAddress, address _nftAddress) public { + require( + _tokenAddress.isContract(), + "_tokenAddress must be a contract address" + ); + require( + _nftAddress.isContract(), + "_nftAddress must be a contract address" + ); + tokenAddress = _tokenAddress; + nftAddress = _nftAddress; + } + + /** + * Get Owner Claims + * + * @param owner - Claim Owner Address + */ + function ownerClaims(address owner) + external + view + returns (uint256[] memory) + { + require(owner != address(0), "Owner address cannot be 0"); + return _ownerClaims[owner]; + } + + /** + * Get NFT Claims + * + * @param nftId - NFT ID + */ + function nftClaims(uint256 nftId) external view returns (uint256[] memory) { + require(nftId != 0, "nftId cannot be 0"); + return _nftClaims[nftId]; + } + + /** + * Get Amount Claimed + * + * @param index - Claim Index + */ + function claimed(uint256 index) external view returns (uint256) { + return claims[index].amountClaimed; + } + + /** + * Get Total Claim Amount + * + * @param index - Claim Index + */ + function totalAmount(uint256 index) external view returns (uint256) { + return claims[index].totalAmount; + } + + /** + * Get Time Periods of Claim + * + * @param index - Claim Index + */ + function timePeriods(uint256 index) + external + view + returns (uint256[] memory) + { + return claims[index].timePeriods; + } + + /** + * Get Token Amounts of Claim + * + * @param index - Claim Index + */ + function tokenAmounts(uint256 index) + external + view + returns (uint256[] memory) + { + return claims[index].tokenAmounts; + } + + /** + * Create a Claim - To Vest Tokens to NFT + * + * @param _nftId - Tokens will be claimed by owner of _nftId + * @param _startBlock - Block Number to start vesting from + * @param _stopBlock - Block Number to end vesting at (Release all tokens) + * @param _totalAmount - Total Amount to be Vested + * @param _blockTime - Block Time (used for predicting _timePeriods) + */ + function createClaim( + uint256 _nftId, + uint256 _startBlock, + uint256 _stopBlock, + uint256 _totalAmount, + uint256 _blockTime + ) external returns (bool) { + require(_nftId != 0, "Cannot Vest to NFT 0"); + require( + _stopBlock > _startBlock, + "_stopBlock must be greater than _startBlock" + ); + require(tokenAddress.isContract(), "Invalid tokenAddress"); + require(_totalAmount > 0, "Provide Token Amounts to Vest"); + require( + ERC20(tokenAddress).allowance(msg.sender, address(this)) >= + _totalAmount, + "Provide token allowance to SuperVestStream contract" + ); + // Calculate estimated epoch for _startBlock + uint256 startTime; + // ================================== + // Commented code can be used if we want + // startTime to be now in the event of + // _startBlock being behind current block + // ================================== + // + // if ((_startBlock - block.number) <= 0) { + // startTime = block.timestamp; + // } else { + startTime = + block.timestamp + + (_blockTime * (_startBlock - block.number)); + // } + // Calculate _timePeriods & _tokenAmounts + uint256 diff = _stopBlock - _startBlock; + uint256 amountPerBlock = _totalAmount / diff; + uint256[] memory _timePeriods = new uint256[](diff); + uint256[] memory _tokenAmounts = new uint256[](diff); + _timePeriods[0] = startTime; + _tokenAmounts[0] = amountPerBlock; + for (uint256 i = 1; i < diff; i++) { + _timePeriods[i] = _timePeriods[i - 1] + _blockTime; + _tokenAmounts[i] = amountPerBlock; + } + // Transfer Tokens to SuperVestStream + ERC20(tokenAddress).transferFrom( + msg.sender, + address(this), + _totalAmount + ); + // Create Claim + Claim memory claim = + Claim({ + owner: msg.sender, + nftId: _nftId, + timePeriods: _timePeriods, + tokenAmounts: _tokenAmounts, + totalAmount: _totalAmount, + amountClaimed: 0, + periodsClaimed: 0 + }); + claims.push(claim); + uint256 index = claims.length - 1; + // Map Claim Index to Owner & Beneficiary + _ownerClaims[msg.sender].push(index); + _nftClaims[_nftId].push(index); + emit ClaimCreated(msg.sender, _nftId, _totalAmount, index); + return true; + } + + /** + * Claim Tokens + * + * @param index - Index of the Claim + */ + function claim(uint256 index) external { + Claim storage claim = claims[index]; + // Check if msg.sender is the owner of the NFT + require( + msg.sender == ERC721(nftAddress).ownerOf(claim.nftId), + "msg.sender must own the NFT" + ); + // Check if anything is left to release + require( + claim.periodsClaimed < claim.timePeriods.length, + "Nothing to release" + ); + // Calculate releasable amount + uint256 amount = 0; + for ( + uint256 i = claim.periodsClaimed; + i < claim.timePeriods.length; + i++ + ) { + if (claim.timePeriods[i] <= block.timestamp) { + amount = amount.add(claim.tokenAmounts[i]); + claim.periodsClaimed = claim.periodsClaimed.add(1); + } else { + break; + } + } + // If there is any amount to release + require(amount > 0, "Nothing to release"); + // Transfer Tokens from Owner to Beneficiary + ERC20(tokenAddress).transfer(msg.sender, amount); + claim.amountClaimed = claim.amountClaimed.add(amount); + emit Claimed(claim.owner, claim.nftId, msg.sender, amount, index); + } + + /** + * Get Amount of tokens that can be claimed + * + * @param index - Index of the Claim + */ + function claimableAmount(uint256 index) public view returns (uint256) { + Claim storage claim = claims[index]; + // Calculate Claimable Amount + uint256 amount = 0; + for ( + uint256 i = claim.periodsClaimed; + i < claim.timePeriods.length; + i++ + ) { + if (claim.timePeriods[i] <= block.timestamp) { + amount = amount.add(claim.tokenAmounts[i]); + } else { + break; + } + } + return amount; + } +} diff --git a/contracts/SuperToken.sol b/contracts/SuperToken.sol new file mode 100644 index 0000000..7141669 --- /dev/null +++ b/contracts/SuperToken.sol @@ -0,0 +1,542 @@ +pragma solidity ^0.6.2; + +/* + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with GSN meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +contract Context { + // Empty internal constructor, to prevent people from mistakenly deploying + // an instance of this contract, which should be used via inheritance. + constructor() internal {} + + // solhint-disable-previous-line no-empty-blocks + + function _msgSender() internal view returns (address payable) { + return msg.sender; + } + + function _msgData() internal view returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } +} + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. Does not include + * the optional functions; to access them see {ERC20Detailed}. + */ +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 + ); +} + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + * + * _Available since v2.4.0._ + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * _Available since v2.4.0._ + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * _Available since v2.4.0._ + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} + +/** + * @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}. + * For a generic mechanism see {ERC20Mintable}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of 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. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20 is Context, IERC20 { + using SafeMath for uint256; + + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) + public + override + returns (bool) + { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) + public + view + override + returns (uint256) + { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) + public + override + returns (bool) + { + _approve(_msgSender(), spender, amount); + 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}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for `sender`'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public override returns (bool) { + _transfer(sender, recipient, amount); + _approve( + sender, + _msgSender(), + _allowances[sender][_msgSender()].sub( + amount, + "ERC20: transfer amount exceeds allowance" + ) + ); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) + public + returns (bool) + { + _approve( + _msgSender(), + spender, + _allowances[_msgSender()][spender].add(addedValue) + ); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) + public + returns (bool) + { + _approve( + _msgSender(), + spender, + _allowances[_msgSender()][spender].sub( + subtractedValue, + "ERC20: decreased allowance below zero" + ) + ); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _balances[sender] = _balances[sender].sub( + amount, + "ERC20: transfer amount exceeds balance" + ); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal { + require(account != address(0), "ERC20: mint to the zero address"); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. + * + * This is 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. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } +} + +contract SuperToken is ERC20 { + uint256 public totalTokensAmount = 50000000; + + string public name = "SUPER Token"; + string public symbol = "SUPER"; + uint8 public decimals = 18; + + constructor() public { + _mint(msg.sender, totalTokensAmount * (10**uint256(decimals))); + } +} diff --git a/contracts/SuperVestCliff.sol b/contracts/SuperVestCliff.sol new file mode 100644 index 0000000..ff323bb --- /dev/null +++ b/contracts/SuperVestCliff.sol @@ -0,0 +1,233 @@ +pragma solidity ^0.6.2; + +// REMIX +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/token/ERC20/ERC20.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/math/SafeMath.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/utils/Address.sol"; + +// TRUFFLE +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +contract SuperVestCliff { + using SafeMath for uint256; + using Address for address; + + address public tokenAddress; + + event Claimed( + address owner, + address beneficiary, + uint256 amount, + uint256 index + ); + event ClaimCreated(address owner, address beneficiary, uint256 index); + + struct Claim { + address owner; + address beneficiary; + uint256[] timePeriods; + uint256[] tokenAmounts; + uint256 totalAmount; + uint256 amountClaimed; + uint256 periodsClaimed; + } + Claim[] private claims; + + mapping(address => uint256[]) private _ownerClaims; + mapping(address => uint256[]) private _beneficiaryClaims; + + constructor(address _tokenAddress) public { + tokenAddress = _tokenAddress; + } + + /** + * Get Owner Claims + * + * @param owner - Claim Owner Address + */ + function ownerClaims(address owner) + external + view + returns (uint256[] memory) + { + require(owner != address(0), "Owner address cannot be 0"); + return _ownerClaims[owner]; + } + + /** + * Get Beneficiary Claims + * + * @param beneficiary - Claim Owner Address + */ + function beneficiaryClaims(address beneficiary) + external + view + returns (uint256[] memory) + { + require(beneficiary != address(0), "Beneficiary address cannot be 0"); + return _beneficiaryClaims[beneficiary]; + } + + /** + * Get Amount Claimed + * + * @param index - Claim Index + */ + function claimed(uint256 index) external view returns (uint256) { + return claims[index].amountClaimed; + } + + /** + * Get Total Claim Amount + * + * @param index - Claim Index + */ + function totalAmount(uint256 index) external view returns (uint256) { + return claims[index].totalAmount; + } + + /** + * Get Time Periods of Claim + * + * @param index - Claim Index + */ + function timePeriods(uint256 index) + external + view + returns (uint256[] memory) + { + return claims[index].timePeriods; + } + + /** + * Get Token Amounts of Claim + * + * @param index - Claim Index + */ + function tokenAmounts(uint256 index) + external + view + returns (uint256[] memory) + { + return claims[index].tokenAmounts; + } + + /** + * Create a Claim - To Vest Tokens to Beneficiary + * + * @param _beneficiary - Tokens will be claimed by _beneficiary + * @param _timePeriods - uint256 Array of Epochs + * @param _tokenAmounts - uin256 Array of Amounts to transfer at each time period + */ + function createClaim( + address _beneficiary, + uint256[] memory _timePeriods, + uint256[] memory _tokenAmounts + ) public returns (bool) { + require( + _timePeriods.length == _tokenAmounts.length, + "_timePeriods & _tokenAmounts length mismatch" + ); + require(tokenAddress.isContract(), "Invalid tokenAddress"); + require(_beneficiary != address(0), "Cannot Vest to address 0"); + // Calculate total amount + uint256 _totalAmount = 0; + for (uint256 i = 0; i < _tokenAmounts.length; i++) { + _totalAmount = _totalAmount.add(_tokenAmounts[i]); + } + require(_totalAmount > 0, "Provide Token Amounts to Vest"); + require( + ERC20(tokenAddress).allowance(msg.sender, address(this)) >= + _totalAmount, + "Provide token allowance to SuperStreamClaim contract" + ); + // Transfer Tokens to SuperStreamClaim + ERC20(tokenAddress).transferFrom( + msg.sender, + address(this), + _totalAmount + ); + // Create Claim + Claim memory claim = + Claim({ + owner: msg.sender, + beneficiary: _beneficiary, + timePeriods: _timePeriods, + tokenAmounts: _tokenAmounts, + totalAmount: _totalAmount, + amountClaimed: 0, + periodsClaimed: 0 + }); + claims.push(claim); + uint256 index = claims.length - 1; + // Map Claim Index to Owner & Beneficiary + _ownerClaims[msg.sender].push(index); + _beneficiaryClaims[_beneficiary].push(index); + emit ClaimCreated(msg.sender, _beneficiary, index); + return true; + } + + /** + * Claim Tokens + * + * @param index - Index of the Claim + */ + function claim(uint256 index) external { + Claim storage claim = claims[index]; + // Check if msg.sender is the beneficiary + require( + claim.beneficiary == msg.sender, + "Only beneficiary can claim tokens" + ); + // Check if anything is left to release + require( + claim.periodsClaimed < claim.timePeriods.length, + "Nothing to release" + ); + // Calculate releasable amount + uint256 amount = 0; + for ( + uint256 i = claim.periodsClaimed; + i < claim.timePeriods.length; + i++ + ) { + if (claim.timePeriods[i] <= block.timestamp) { + amount = amount.add(claim.tokenAmounts[i]); + claim.periodsClaimed = claim.periodsClaimed.add(1); + } else { + break; + } + } + // If there is any amount to release + require(amount > 0, "Nothing to release"); + // Transfer Tokens from Owner to Beneficiary + ERC20(tokenAddress).transfer(claim.beneficiary, amount); + claim.amountClaimed = claim.amountClaimed.add(amount); + emit Claimed(claim.owner, claim.beneficiary, amount, index); + } + + /** + * Get Amount of tokens that can be claimed + * + * @param index - Index of the Claim + */ + function claimableAmount(uint256 index) public view returns (uint256) { + Claim storage claim = claims[index]; + // Calculate Claimable Amount + uint256 amount = 0; + for ( + uint256 i = claim.periodsClaimed; + i < claim.timePeriods.length; + i++ + ) { + if (claim.timePeriods[i] <= block.timestamp) { + amount = amount.add(claim.tokenAmounts[i]); + } else { + break; + } + } + return amount; + } +} diff --git a/contracts/SuperVestStream.sol b/contracts/SuperVestStream.sol new file mode 100644 index 0000000..0fc4794 --- /dev/null +++ b/contracts/SuperVestStream.sol @@ -0,0 +1,258 @@ +pragma solidity ^0.6.2; + +// REMIX +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/token/ERC20/ERC20.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/math/SafeMath.sol"; +// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/utils/Address.sol"; + +// TRUFFLE +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +contract SuperVestStream { + using SafeMath for uint256; + using Address for address; + + address public tokenAddress; + + event Claimed( + address owner, + address beneficiary, + uint256 amount, + uint256 index + ); + event ClaimCreated(address owner, address beneficiary, uint256 index); + + struct Claim { + address owner; + address beneficiary; + uint256[] timePeriods; + uint256[] tokenAmounts; + uint256 totalAmount; + uint256 amountClaimed; + uint256 periodsClaimed; + } + Claim[] private claims; + + mapping(address => uint256[]) private _ownerClaims; + mapping(address => uint256[]) private _beneficiaryClaims; + + constructor(address _tokenAddress) public { + tokenAddress = _tokenAddress; + } + + /** + * Get Owner Claims + * + * @param owner - Claim Owner Address + */ + function ownerClaims(address owner) + external + view + returns (uint256[] memory) + { + require(owner != address(0), "Owner address cannot be 0"); + return _ownerClaims[owner]; + } + + /** + * Get Beneficiary Claims + * + * @param beneficiary - Claim Owner Address + */ + function beneficiaryClaims(address beneficiary) + external + view + returns (uint256[] memory) + { + require(beneficiary != address(0), "Beneficiary address cannot be 0"); + return _beneficiaryClaims[beneficiary]; + } + + /** + * Get Amount Claimed + * + * @param index - Claim Index + */ + function claimed(uint256 index) external view returns (uint256) { + return claims[index].amountClaimed; + } + + /** + * Get Total Claim Amount + * + * @param index - Claim Index + */ + function totalAmount(uint256 index) external view returns (uint256) { + return claims[index].totalAmount; + } + + /** + * Get Time Periods of Claim + * + * @param index - Claim Index + */ + function timePeriods(uint256 index) + external + view + returns (uint256[] memory) + { + return claims[index].timePeriods; + } + + /** + * Get Token Amounts of Claim + * + * @param index - Claim Index + */ + function tokenAmounts(uint256 index) + external + view + returns (uint256[] memory) + { + return claims[index].tokenAmounts; + } + + /** + * Create a Claim - To Vest Tokens to Beneficiary + * + * @param _beneficiary - Tokens will be claimed by _beneficiary + * @param _startBlock - Block Number to start vesting from + * @param _stopBlock - Block Number to end vesting at (Release all tokens) + * @param _totalAmount - Total Amount to be Vested + * @param _blockTime - Block Time (used for predicting _timePeriods) + */ + function createClaim( + address _beneficiary, + uint256 _startBlock, + uint256 _stopBlock, + uint256 _totalAmount, + uint256 _blockTime + ) external returns (bool) { + require( + _stopBlock > _startBlock, + "_stopBlock must be greater than _startBlock" + ); + require(tokenAddress.isContract(), "Invalid tokenAddress"); + require(_beneficiary != address(0), "Cannot Vest to address 0"); + require(_totalAmount > 0, "Provide Token Amounts to Vest"); + require( + ERC20(tokenAddress).allowance(msg.sender, address(this)) >= + _totalAmount, + "Provide token allowance to SuperVestStream contract" + ); + // Calculate estimated epoch for _startBlock + uint256 startTime; + // ================================== + // Commented code can be used if we want + // startTime to be now in the event of + // _startBlock being behind current block + // ================================== + // + // if ((_startBlock - block.number) <= 0) { + // startTime = block.timestamp; + // } else { + startTime = + block.timestamp + + (_blockTime * (_startBlock - block.number)); + // } + // Calculate _timePeriods & _tokenAmounts + uint256 diff = _stopBlock - _startBlock; + uint256 amountPerBlock = _totalAmount / diff; + uint256[] memory _timePeriods = new uint256[](diff); + uint256[] memory _tokenAmounts = new uint256[](diff); + _timePeriods[0] = startTime; + _tokenAmounts[0] = amountPerBlock; + for (uint256 i = 1; i < diff; i++) { + _timePeriods[i] = _timePeriods[i - 1] + _blockTime; + _tokenAmounts[i] = amountPerBlock; + } + // Transfer Tokens to SuperVestStream + ERC20(tokenAddress).transferFrom( + msg.sender, + address(this), + _totalAmount + ); + // Create Claim + Claim memory claim = + Claim({ + owner: msg.sender, + beneficiary: _beneficiary, + timePeriods: _timePeriods, + tokenAmounts: _tokenAmounts, + totalAmount: _totalAmount, + amountClaimed: 0, + periodsClaimed: 0 + }); + claims.push(claim); + uint256 index = claims.length - 1; + // Map Claim Index to Owner & Beneficiary + _ownerClaims[msg.sender].push(index); + _beneficiaryClaims[_beneficiary].push(index); + emit ClaimCreated(msg.sender, _beneficiary, index); + return true; + } + + /** + * Claim Tokens + * + * @param index - Index of the Claim + */ + function claim(uint256 index) external { + Claim storage claim = claims[index]; + // Check if msg.sender is the beneficiary + require( + claim.beneficiary == msg.sender, + "Only beneficiary can claim tokens" + ); + // Check if anything is left to release + require( + claim.periodsClaimed < claim.timePeriods.length, + "Nothing to release" + ); + // Calculate releasable amount + uint256 amount = 0; + for ( + uint256 i = claim.periodsClaimed; + i < claim.timePeriods.length; + i++ + ) { + if (claim.timePeriods[i] <= block.timestamp) { + amount = amount.add(claim.tokenAmounts[i]); + claim.periodsClaimed = claim.periodsClaimed.add(1); + } else { + break; + } + } + // If there is any amount to release + require(amount > 0, "Nothing to release"); + // Transfer Tokens from Owner to Beneficiary + ERC20(tokenAddress).transfer(claim.beneficiary, amount); + claim.amountClaimed = claim.amountClaimed.add(amount); + emit Claimed(claim.owner, claim.beneficiary, amount, index); + } + + /** + * Get Amount of tokens that can be claimed + * + * @param index - Index of the Claim + */ + function claimableAmount(uint256 index) public view returns (uint256) { + Claim storage claim = claims[index]; + // Calculate Claimable Amount + uint256 amount = 0; + for ( + uint256 i = claim.periodsClaimed; + i < claim.timePeriods.length; + i++ + ) { + if (claim.timePeriods[i] <= block.timestamp) { + amount = amount.add(claim.tokenAmounts[i]); + } else { + break; + } + } + return amount; + } +}