// SPDX-License-Identifier: Apache-2.0 // https://docs.soliditylang.org/en/v0.8.10/style-guide.html pragma solidity ^0.8.10; import "./IAccounts.sol"; import "./ILockedGold.sol"; import "./IElection.sol"; import "./IRegistry.sol"; import "./IValidators.sol"; //import "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; contract SpiralsStaking is PausableUpgradeable { address public validatorGroup; address public owner; uint256 public bufferPool; uint256 public totalPendingWithdrawal; IRegistry constant c_celoRegistry = IRegistry(0x000000000000000000000000000000000000ce10); struct StakerInfo { uint256 stakedValue; uint256 withdrawalValue; uint256 withdrawalTimestamp; } mapping(address => StakerInfo) stakers; uint256 public newVariable; event Deposit( address indexed sender, uint256 indexed amount, bool isBuffer ); event UserCeloStaked( address indexed _address, address indexed _validatorGroup, uint256 indexed amount ); event ProtocolCeloActivated( address indexed _validatorGroup, uint256 indexed amount ); event UserCeloUnstaked( address indexed _address, address indexed _validatorGroup, uint256 indexed amount ); event UserCeloWithdrawn(address indexed _address, uint256 indexed amount); event ProtocolCeloWithdrawn( uint256 indexed totalAmount, uint256 indexed timestamp ); function initialize(address _validatorGroup) public initializer { validatorGroup = _validatorGroup; owner = msg.sender; require(getAccounts().createAccount(), "CREATE_ACCOUNT_FAILED"); } receive() external payable { emit Deposit(msg.sender, msg.value, false); } /// @dev Modifier for checking whether function caller is `_owner`. modifier onlyOwner() { require(msg.sender == owner, "Only owner can call this function!"); _; } /// @notice Allows deposits into the protocol's buffer pool to facilitate /// unstaking. function depositBP() public payable { bufferPool += msg.value; emit Deposit(msg.sender, msg.value, true); } /* * STAKING */ /// @notice Main function for staking with Spirals protocol /// @dev function stake() external payable { require(msg.value > 0, "STAKING_ZERO"); lock(msg.value); vote(msg.value); stakers[msg.sender].stakedValue += msg.value; emit UserCeloStaked(msg.sender, validatorGroup, msg.value); } /// @dev Helper function for locking CELO function lock(uint256 _value) internal { require(_value > 0, "LOCKING_ZERO"); 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)), "INVALID_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, uint256 ) { (address[] memory validatorGroups, ) = getElection() .getTotalVotesForEligibleValidatorGroups(); // sorted by votes desc address lesser = address(0); address greater = address(0); uint256 index = 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]; } index = i; break; } } return (lesser, greater, index); } /* * UNSTAKING */ /// @notice Main function for unstaking from Spirals protocol /// @dev A particular user calling "unstake" adds a pending withdrawal /// for Spirals in the Celo smart contracts. After calling this function, /// a user officially unstakes but still needs to "withdraw" after /// the unlocking period is over. function unstake(uint256 _value) public virtual { require( stakers[msg.sender].stakedValue >= _value, "EXCEEDS_USER_STAKE" ); uint256 activeVotes = getActiveVotes(); (uint256 pendingVotes, ) = getPendingVotes(); require(activeVotes + pendingVotes >= _value, "EXCEEDS_PROTOCOL_STAKE"); // Can only support 1 outstanding unstake request at a time (without // rebuilding all of how Celo unstaking works) require( stakers[msg.sender].withdrawalValue == 0, "OUTSTANDING_PENDING_WITHDRAWAL" ); if (activeVotes >= _value) { revokeActive(_value); } else { revokePending(_value); } unlock(_value); StakerInfo memory newStaker = stakers[msg.sender]; newStaker.stakedValue -= _value; newStaker.withdrawalValue = _value; newStaker.withdrawalTimestamp = block.timestamp + getLockedGold().unlockingPeriod(); totalPendingWithdrawal += _value; stakers[msg.sender] = newStaker; emit UserCeloUnstaked(msg.sender, validatorGroup, _value); } /// @notice Helper function for revoking active votes CELO function revokeActive(uint256 _value) internal { (address lesser, address greater, uint256 index) = getLesserGreater(); require( getElection().revokeActive( validatorGroup, _value, lesser, greater, index ) ); } /// @notice Helper function for revoking pending votes CELO function revokePending(uint256 _value) internal { (address lesser, address greater, uint256 index) = getLesserGreater(); require( getElection().revokePending( validatorGroup, _value, lesser, greater, index ) ); } /// @notice Helper function for unlocking CELO function unlock(uint256 _value) internal { getLockedGold().unlock(_value); } /// @notice Allow user to withdraw their stake back to wallet. /// @dev Withdraws from this contracts balance directly. function withdraw() public virtual { StakerInfo memory s = stakers[msg.sender]; require(s.withdrawalValue > 0, "NO_PENDING_WITHDRAWALS"); require(userCanWithdraw(msg.sender), "WITHDRAWAL_NOT_READY"); payable(msg.sender).transfer(s.withdrawalValue); // should fail if protocol doesn't have enough emit UserCeloWithdrawn(msg.sender, s.withdrawalValue); totalPendingWithdrawal -= s.withdrawalValue; s.withdrawalValue = 0; s.withdrawalTimestamp = 0; stakers[msg.sender] = s; } /// @notice Helper function for checking whether protocol can support /// a user who wants to withdraw. function userCanWithdraw(address _address) public view returns (bool) { StakerInfo memory s = stakers[_address]; return address(this).balance >= s.withdrawalValue && s.withdrawalTimestamp <= block.timestamp; } /* * ADMIN */ /// @notice Activates pending votes (if ready) with a given validator group. /// Onus is on the protocol owners to activate to make sure CELO /// staked with protocol is staked on CELO L1. function activateForProtocol() 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 ProtocolCeloActivated(validatorGroup, pendingVotes); } /// @notice Withdraws CELO from any pending withdrawals that are available. /// Onus is on the protocol owners to withdraw CELO from LockedGold function withdrawForProtocol() external onlyOwner { (uint256[] memory values, uint256[] memory timestamps) = getLockedGold() .getPendingWithdrawals(address(this)); // loop backwards so withdrawing at a single index doesn't shift indices uint256 withdrawnTotal; for (uint256 i = timestamps.length; i > 0; i--) { if (block.timestamp >= timestamps[i - 1]) { getLockedGold().withdraw(i - 1); withdrawnTotal += values[i - 1]; } } emit ProtocolCeloWithdrawn(withdrawnTotal, block.timestamp); } /// @notice Convenience function for withdrawing the first /// pending withdrawal request from the protocol. function withdrawFirstRequestForProtocol() external onlyOwner { getLockedGold().withdraw(0); } /* * OTHER */ /// @notice Returns all details relevant to an account staking with Spirals. function getAccount(address _address) public view returns ( uint256, uint256, uint256 ) { StakerInfo memory s = stakers[_address]; return (s.stakedValue, s.withdrawalValue, s.withdrawalTimestamp); } /// @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_VALIDATOR_GROUP" ); validatorGroup = _newValidatorGroup; } /// @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) ); } /* * 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); } }