// SPDX-License-Identifier: Apache-2.0 // https://docs.soliditylang.org/en/v0.8.10/style-guide.html pragma solidity ^0.8.10; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "./IAccounts.sol"; import "./ILockedGold.sol"; import "./IElection.sol"; import "./IRegistry.sol"; import "./IValidators.sol"; contract SpiralsStaking { using SafeMath for uint256; address public validatorGroup; address public owner; uint256 public totalStaked; IRegistry constant c_celoRegistry = IRegistry(0x000000000000000000000000000000000000ce10); mapping(address => uint256) stakeByAccount; event VotesCast( address indexed _address, address indexed _validatorGroup, uint256 indexed amount ); event VotesActivated( address indexed _validatorGroup, uint256 indexed amount ); event Unstake( address indexed _address, address indexed _validatorGroup, uint256 indexed amount ); constructor(address _validatorGroup) {} function initialize(address _validatorGroup) public { validatorGroup = _validatorGroup; owner = msg.sender; require(getAccounts().createAccount(), "CREATE_ACCOUNT"); } /// @dev Modifier for checking whether function caller is `_owner`. modifier onlyOwner() { require(msg.sender == owner, "Only owner can call this function!"); _; } /* * STAKING */ /// @notice Main function for staking with Spirals protocol /// @dev function stake() external payable { require(msg.value != 0, "NO_VALUE_STAKED"); lock(msg.value); vote(msg.value); stakeByAccount[msg.sender] = stakeByAccount[msg.sender].add(msg.value); totalStaked = totalStaked.add(msg.value); // all pending -> active for this group emit VotesCast(msg.sender, validatorGroup, msg.value); } /// @dev Helper function for locking CELO function lock(uint256 _value) internal { getLockedGold().lock{value: _value}(); } /// @dev Helper function for casting votes with a given validator group function vote(uint256 _value) internal { (address lesser, address greater) = getLesserGreater(); require( !(lesser == address(0) && greater == address(0)), "NO_LESSER_GREATER" ); // Can't both be null address require( getElection().vote(validatorGroup, _value, lesser, greater), "VOTE_FAILED" ); } /// @dev Helper function for getting the 2 validator groups that /// our target validator group is sandwiched between. function getLesserGreater() internal view returns (address, address) { (address[] memory validatorGroups, ) = getElection() .getTotalVotesForEligibleValidatorGroups(); // sorted by votes desc address lesser = address(0); address greater = address(0); for (uint256 i = 0; i < validatorGroups.length; i++) { if (validatorGroup == validatorGroups[i]) { if (i > 0) { greater = validatorGroups[i - 1]; } if (i < validatorGroups.length - 1) { lesser = validatorGroups[i + 1]; } break; } } return (lesser, greater); } /// @dev Activates pending votes (if ready) with a given validator group. function activate() external onlyOwner { IElection c_election = getElection(); require( c_election.hasActivatablePendingVotes( address(this), validatorGroup ), "NOT_READY_TO_ACTIVATE" ); uint256 pendingVotes = getElection().getPendingVotesForGroupByAccount( validatorGroup, address(this) ); require(c_election.activate(validatorGroup), "ACTIVATE_FAILED"); // all pending -> active for this group emit VotesActivated(validatorGroup, pendingVotes); } /* * UNSTAKING */ // function unstake() public {} // function revoke() public {} // function unlock() public {} // function withdraw() public {} /* * OTHER */ /// @notice For updating with validator group we stake with. Performs /// some simple checks to make sure address given is an eligible /// validator group (limited to 1 for now). function setValidatorGroup(address _newValidatorGroup) external onlyOwner { require( getValidators().isValidatorGroup(_newValidatorGroup), "NOT_VALIDATOR_GROUP" ); require( getElection().getGroupEligibility(_newValidatorGroup), "NOT_ELIGIBLE_VG" ); validatorGroup = _newValidatorGroup; } /// @notice Get active votes (staked + rewards) for this smart contract. function getRewards() public view returns (uint256) { uint256 activeVotes = getActiveVotes(); (uint256 pendingVotes, ) = getPendingVotes(); require( activeVotes.add(pendingVotes) >= totalStaked, "NEGATIVE_REWARDS" ); return activeVotes.add(pendingVotes).sub(totalStaked); } /// @notice Get pending votes for this smart contract. function getPendingVotes() public view returns (uint256, bool) { return ( getElection().getPendingVotesForGroupByAccount( validatorGroup, address(this) ), getElection().hasActivatablePendingVotes( validatorGroup, address(this) ) ); } /// @notice Get active votes (staked + rewards) for this smart contract. function getActiveVotes() public view returns (uint256) { return getElection().getActiveVotesForGroupByAccount( validatorGroup, address(this) ); } /// @notice Returns the amount a certain address is currently staking /// with Spirals. function getStakeForAccount(address _address) public view returns (uint256) { return stakeByAccount[_address]; } /* * CELO SMART CONTRACT HELPERS */ /// @dev Returns a Accounts.sol interface for interacting with the smart contract. function getAccounts() internal view returns (IAccounts) { address accountsAddr = c_celoRegistry.getAddressForStringOrDie( "Accounts" ); return IAccounts(accountsAddr); } /// @dev Returns an Election.sol interface for interacting with the smart contract. function getElection() internal view returns (IElection) { address electionAddr = c_celoRegistry.getAddressForStringOrDie( "Election" ); return IElection(electionAddr); } /// @dev Returns a LockedGold.sol interface for interacting with the smart contract. function getLockedGold() internal view returns (ILockedGold) { address lockedGoldAddr = c_celoRegistry.getAddressForStringOrDie( "LockedGold" ); return ILockedGold(lockedGoldAddr); } /// @dev Returns a Validators.sol interface for interacting with the smart contract. function getValidators() internal view returns (IValidators) { address validatorsAddr = c_celoRegistry.getAddressForStringOrDie( "Validators" ); return IValidators(validatorsAddr); } }