From 9b8c4eb012325218bbdb33c13dd84ce6ddf2e5fa Mon Sep 17 00:00:00 2001 From: bahadylbekov <33404905+bahadylbekov@users.noreply.github.com> Date: Fri, 30 Oct 2020 06:41:19 +0300 Subject: [PATCH] add: dione token, dione staking contract, timelock contract, fix wallet and consensus leader --- .gitignore | 3 +- consensus/leader.go | 2 +- eth-contracts/contracts/DIoneStaking.sol | 127 ++++++++++++ eth-contracts/contracts/DioneToken.sol | 243 +++++++++++++++++++++++ eth-contracts/contracts/Timelock.sol | 132 ++++++++++++ eth-contracts/truffle-config.js | 2 +- wallet/wallet.go | 2 +- 7 files changed, 507 insertions(+), 4 deletions(-) create mode 100644 eth-contracts/contracts/DIoneStaking.sol create mode 100644 eth-contracts/contracts/DioneToken.sol create mode 100644 eth-contracts/contracts/Timelock.sol diff --git a/.gitignore b/.gitignore index e905fa3..3edbbbe 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ .env eth-contracts/node_modules /dione -/.dione-config.toml \ No newline at end of file +/.dione-config.toml +eth-contracts/build \ No newline at end of file diff --git a/consensus/leader.go b/consensus/leader.go index fb1c76b..73ba3d9 100644 --- a/consensus/leader.go +++ b/consensus/leader.go @@ -75,7 +75,7 @@ func IsRoundWinner(ctx context.Context, round types.TaskEpoch, } ep := &types.ElectionProof{VRFProof: vrfout} - j := ep.ComputeWinCount(mbi.MinerPower, mbi.NetworkPower) + j := ep.ComputeWinCount(mbi.MinerStake, mbi.NetworkStake) ep.WinCount = j if j < 1 { return nil, nil diff --git a/eth-contracts/contracts/DIoneStaking.sol b/eth-contracts/contracts/DIoneStaking.sol new file mode 100644 index 0000000..f4f1426 --- /dev/null +++ b/eth-contracts/contracts/DIoneStaking.sol @@ -0,0 +1,127 @@ +pragma solidity 0.6.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "./DioneToken.sol"; + +// DioneStaking is the main contract for Dione oracle network Proof-of-Stake mechanism +// +// Note that it's ownable and the owner has power over changing miner rewards and minimum DIONE stake +// The ownership of the contract would be transfered to 24 hours Timelock +contract DioneStaking is Ownable, ReentrancyGuard { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + // MinerInfo contains total DIONEs staked by miner, how much tasks been already computed + // and timestamp of first deposit + struct MinerInfo { + uint256 amount; // How many DIONE tokens was staked by the miner. + uint256 firstStakeBlock; // First block for miner DIONE tokens reward. + uint256 lastRewardBlock; // Last block for miner DIONE tokens reward + } + + DioneToken public dione; + // Agregator contract address. + address public aggregatorAddr; + // Miner rewards in DIONE tokens. + uint256 public minerReward; + // The block number when DIONE mining starts. + uint256 public startBlock; + // Minimum amount of staked DIONE tokens required to start mining + uint256 public minimumStake; + // Total amount of DIONE tokens staked + uint256 private _totalStake; + + // Info of each miner that stakes DIONE tokens. + mapping (address => MinerInfo) public minerInfo; + + event Stake(address indexed miner, uint256 amount); + event Withdraw(address indexed miner, uint256 amount); + event Mine(address indexed miner, uint256 blockNumber); + + constructor( + DioneToken _dione, + address _aggregatorAddr, + uint256 _minerReward, + uint256 _startBlock, + uint256 _minimumStake; + ) public { + dione = _dione; + aggregatorAddr = _aggregatorAddr; + minerReward = _minerReward; + startBlock = _startBlock; + minimumStake = _minimumStake; + } + + // Mine new dione oracle task, only can be executed by aggregator contract + function mine(address _minerAddr) public nonReentrant { + require(msg.sender == aggregatorAddr, "not aggregator contract"); + MinerInfo storage miner = minerInfo[_minerAddr]; + require(miner.amount >= minimumStake) + dione.mint(_minerAddr, minerReward); + miner.lastRewardBlock = block.number; + emit Mine(_minerAddr, block.number) + } + + // Mine new dione oracle task and stake miner reward, only can be executed by aggregator contract + function mineAndStake(address _minerAddr) public nonReentrant { + require(msg.sender == aggregatorAddr, "not aggregator contract"); + MinerInfo storage miner = minerInfo[_minerAddr]; + require(miner.amount >= minimumStake) + dione.mint(address(this), minerReward); + _totalStake = _totalStake.add(minerReward); + miner.amount = miner.amount.add(minerReward); + miner.lastRewardBlock = block.number; + emit Mine(_minerAddr, block.number) + } + + // Deposit DIONE tokens to mine on dione network + function stake(uint256 _amount) public nonReentrant { + require(_amount > 0, "Cannot stake 0"); + MinerInfo storage miner = minerInfo[msg.sender]; + _totalStake = _totalStake.add(amount); + miner.amount = miner.amount.add(_amount); + dione.safeTransferFrom(address(msg.sender), address(this), _amount); + if (miner.firstStakeBlock == 0 && miner.amount >= minimumStake) { + miner.firstStakeBlock = block.number > startBlock ? block.number : startBlock; + } + emit Stake(msg.sender, _amount); + } + + // Withdraw DIONE tokens from DioneStaking + function withdraw(uint256 _amount) public nonReentrant { + MinerInfo storage miner = minerInfo[msg.sender]; + require(miner.amount >= _amount, "withdraw: not enough tokens"); + if(_amount > 0) { + _totalStake = _totalStake.sub(amount); + miner.amount = miner.amount.sub(_amount); + dione.safeTransfer(address(msg.sender), _amount); + } + emit Withdraw(msg.sender, _amount); + } + + // Returns total amount of DIONE tokens in PoS mining + function totalStake() external view returns (uint256) { + return _totalStake; + } + + function minerStake(address _minerAddr) external view returns (uint256) { + MinerInfo storage miner = minerInfo[_minerAddr]; + return miner.amount + } + + // Update miner reward in DIONE tokens, only can be executed by owner of the contract + function setMinerReward(uint256 _minerReward) public onlyOwner { + require(_minerReward > 0, "!minerReward-0"); + minerReward = _minerReward; + } + + // Update minimum stake in DIONE tokens for miners, only can be executed by owner of the contract + function setMinimumStake(uint256 _minimumStake) public onlyOwner { + require(_minimumStake > 0, "!minerReward-0"); + minimumStake = _minimumStake; + } +} \ No newline at end of file diff --git a/eth-contracts/contracts/DioneToken.sol b/eth-contracts/contracts/DioneToken.sol new file mode 100644 index 0000000..4470ac8 --- /dev/null +++ b/eth-contracts/contracts/DioneToken.sol @@ -0,0 +1,243 @@ +pragma solidity 0.6.12; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +// DioneToken with Governance. +contract DioneToken is ERC20("DioneToken", "DIONE"), Ownable { + /// @notice Creates `_amount` token to `_to`. Must only be called by the owner (MasterChef). + function mint(address _to, uint256 _amount) public onlyOwner { + _mint(_to, _amount); + _moveDelegates(address(0), _delegates[_to], _amount); + } + + // Copied and modified from YAM code: + // https://github.com/yam-finance/yam-protocol/blob/master/contracts/token/YAMGovernanceStorage.sol + // https://github.com/yam-finance/yam-protocol/blob/master/contracts/token/YAMGovernance.sol + // Which is copied and modified from COMPOUND: + // https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/Comp.sol + + /// @notice A record of each accounts delegate + mapping (address => address) internal _delegates; + + /// @notice A checkpoint for marking number of votes from a given block + struct Checkpoint { + uint32 fromBlock; + uint256 votes; + } + + /// @notice A record of votes checkpoints for each account, by index + mapping (address => mapping (uint32 => Checkpoint)) public checkpoints; + + /// @notice The number of checkpoints for each account + mapping (address => uint32) public numCheckpoints; + + /// @notice The EIP-712 typehash for the contract's domain + bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + + /// @notice The EIP-712 typehash for the delegation struct used by the contract + bytes32 public constant DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + /// @notice A record of states for signing / validating signatures + mapping (address => uint) public nonces; + + /// @notice An event thats emitted when an account changes its delegate + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /// @notice An event thats emitted when a delegate account's vote balance changes + event DelegateVotesChanged(address indexed delegate, uint previousBalance, uint newBalance); + + /** + * @notice Delegate votes from `msg.sender` to `delegatee` + * @param delegator The address to get delegatee for + */ + function delegates(address delegator) + external + view + returns (address) + { + return _delegates[delegator]; + } + + /** + * @notice Delegate votes from `msg.sender` to `delegatee` + * @param delegatee The address to delegate votes to + */ + function delegate(address delegatee) external { + return _delegate(msg.sender, delegatee); + } + + /** + * @notice Delegates votes from signatory to `delegatee` + * @param delegatee The address to delegate votes to + * @param nonce The contract state required to match the signature + * @param expiry The time at which to expire the signature + * @param v The recovery byte of the signature + * @param r Half of the ECDSA signature pair + * @param s Half of the ECDSA signature pair + */ + function delegateBySig( + address delegatee, + uint nonce, + uint expiry, + uint8 v, + bytes32 r, + bytes32 s + ) + external + { + bytes32 domainSeparator = keccak256( + abi.encode( + DOMAIN_TYPEHASH, + keccak256(bytes(name())), + getChainId(), + address(this) + ) + ); + + bytes32 structHash = keccak256( + abi.encode( + DELEGATION_TYPEHASH, + delegatee, + nonce, + expiry + ) + ); + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + domainSeparator, + structHash + ) + ); + + address signatory = ecrecover(digest, v, r, s); + require(signatory != address(0), "DIONE::delegateBySig: invalid signature"); + require(nonce == nonces[signatory]++, "DIONE::delegateBySig: invalid nonce"); + require(now <= expiry, "DIONE::delegateBySig: signature expired"); + return _delegate(signatory, delegatee); + } + + /** + * @notice Gets the current votes balance for `account` + * @param account The address to get votes balance + * @return The number of current votes for `account` + */ + function getCurrentVotes(address account) + external + view + returns (uint256) + { + uint32 nCheckpoints = numCheckpoints[account]; + return nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0; + } + + /** + * @notice Determine the prior number of votes for an account as of a block number + * @dev Block number must be a finalized block or else this function will revert to prevent misinformation. + * @param account The address of the account to check + * @param blockNumber The block number to get the vote balance at + * @return The number of votes the account had as of the given block + */ + function getPriorVotes(address account, uint blockNumber) + external + view + returns (uint256) + { + require(blockNumber < block.number, "DIONE::getPriorVotes: not yet determined"); + + uint32 nCheckpoints = numCheckpoints[account]; + if (nCheckpoints == 0) { + return 0; + } + + // First check most recent balance + if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) { + return checkpoints[account][nCheckpoints - 1].votes; + } + + // Next check implicit zero balance + if (checkpoints[account][0].fromBlock > blockNumber) { + return 0; + } + + uint32 lower = 0; + uint32 upper = nCheckpoints - 1; + while (upper > lower) { + uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow + Checkpoint memory cp = checkpoints[account][center]; + if (cp.fromBlock == blockNumber) { + return cp.votes; + } else if (cp.fromBlock < blockNumber) { + lower = center; + } else { + upper = center - 1; + } + } + return checkpoints[account][lower].votes; + } + + function _delegate(address delegator, address delegatee) + internal + { + address currentDelegate = _delegates[delegator]; + uint256 delegatorBalance = balanceOf(delegator); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveDelegates(currentDelegate, delegatee, delegatorBalance); + } + + function _moveDelegates(address srcRep, address dstRep, uint256 amount) internal { + if (srcRep != dstRep && amount > 0) { + if (srcRep != address(0)) { + // decrease old representative + uint32 srcRepNum = numCheckpoints[srcRep]; + uint256 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0; + uint256 srcRepNew = srcRepOld.sub(amount); + _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew); + } + + if (dstRep != address(0)) { + // increase new representative + uint32 dstRepNum = numCheckpoints[dstRep]; + uint256 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0; + uint256 dstRepNew = dstRepOld.add(amount); + _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew); + } + } + } + + function _writeCheckpoint( + address delegatee, + uint32 nCheckpoints, + uint256 oldVotes, + uint256 newVotes + ) + internal + { + uint32 blockNumber = safe32(block.number, "DIONE::_writeCheckpoint: block number exceeds 32 bits"); + + if (nCheckpoints > 0 && checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber) { + checkpoints[delegatee][nCheckpoints - 1].votes = newVotes; + } else { + checkpoints[delegatee][nCheckpoints] = Checkpoint(blockNumber, newVotes); + numCheckpoints[delegatee] = nCheckpoints + 1; + } + + emit DelegateVotesChanged(delegatee, oldVotes, newVotes); + } + + function safe32(uint n, string memory errorMessage) internal pure returns (uint32) { + require(n < 2**32, errorMessage); + return uint32(n); + } + + function getChainId() internal pure returns (uint) { + uint256 chainId; + assembly { chainId := chainid() } + return chainId; + } +} \ No newline at end of file diff --git a/eth-contracts/contracts/Timelock.sol b/eth-contracts/contracts/Timelock.sol new file mode 100644 index 0000000..d3d702d --- /dev/null +++ b/eth-contracts/contracts/Timelock.sol @@ -0,0 +1,132 @@ +// COPIED FROM https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/GovernorAlpha.sol +// Copyright 2020 Compound Labs, Inc. +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Ctrl+f for XXX to see all the modifications. + +// XXX: pragma solidity ^0.5.16; +pragma solidity 0.6.12; + +// XXX: import "./SafeMath.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +contract Timelock { + using SafeMath for uint; + + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewDelay(uint indexed newDelay); + event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + + uint public constant GRACE_PERIOD = 14 days; + uint public constant MINIMUM_DELAY = 24 hours; + uint public constant MAXIMUM_DELAY = 30 days; + + address public admin; + address public pendingAdmin; + uint public delay; + bool public admin_initialized; + + mapping (bytes32 => bool) public queuedTransactions; + + + constructor(address admin_, uint delay_) public { + require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); + require(delay_ <= MAXIMUM_DELAY, "Timelock::constructor: Delay must not exceed maximum delay."); + + admin = admin_; + delay = delay_; + admin_initialized = false; + } + + // XXX: function() external payable { } + receive() external payable { } + + function setDelay(uint delay_) public { + require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); + require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); + require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); + delay = delay_; + + emit NewDelay(delay); + } + + function acceptAdmin() public { + require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin."); + admin = msg.sender; + pendingAdmin = address(0); + + emit NewAdmin(admin); + } + + function setPendingAdmin(address pendingAdmin_) public { + // allows one time setting of admin for deployment purposes + if (admin_initialized) { + require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock."); + } else { + require(msg.sender == admin, "Timelock::setPendingAdmin: First call must come from admin."); + admin_initialized = true; + } + pendingAdmin = pendingAdmin_; + + emit NewPendingAdmin(pendingAdmin); + } + + function queueTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public returns (bytes32) { + require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); + require(eta >= getBlockTimestamp().add(delay), "Timelock::queueTransaction: Estimated execution block must satisfy delay."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = true; + + emit QueueTransaction(txHash, target, value, signature, data, eta); + return txHash; + } + + function cancelTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public { + require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = false; + + emit CancelTransaction(txHash, target, value, signature, data, eta); + } + + function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public payable returns (bytes memory) { + require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); + require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); + require(getBlockTimestamp() <= eta.add(GRACE_PERIOD), "Timelock::executeTransaction: Transaction is stale."); + + queuedTransactions[txHash] = false; + + bytes memory callData; + + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + } + + // solium-disable-next-line security/no-call-value + (bool success, bytes memory returnData) = target.call{value: value}(callData); + require(success, "Timelock::executeTransaction: Transaction execution reverted."); + + emit ExecuteTransaction(txHash, target, value, signature, data, eta); + + return returnData; + } + + function getBlockTimestamp() internal view returns (uint) { + // solium-disable-next-line security/no-block-members + return block.timestamp; + } +} \ No newline at end of file diff --git a/eth-contracts/truffle-config.js b/eth-contracts/truffle-config.js index 873cf9f..8eec3d0 100644 --- a/eth-contracts/truffle-config.js +++ b/eth-contracts/truffle-config.js @@ -33,7 +33,7 @@ module.exports = { settings: { optimizer: { enabled: true, // Default: false - runs: 0, // Default: 200 + runs: 200, // Default: 200 }, }, }, diff --git a/wallet/wallet.go b/wallet/wallet.go index ae6176e..d343872 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -4,10 +4,10 @@ import ( "context" "sync" - "github.com/Secured-Finance/dione/lib/sigs" "github.com/Secured-Finance/dione/types" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/crypto" + "github.com/filecoin-project/lotus/lib/sigs" "github.com/sirupsen/logrus" "golang.org/x/xerrors" )