diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 4a4bda64..8c2afdaa 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -47,8 +47,8 @@ jobs: - name: Deploy NuCypher Token run: ape run scripts/deploy_nucypher_token.py --network ethereum:local - - name: Deploy Simple PRE - run: ape run scripts/deploy_simple_pre.py --network ethereum:local + - name: Deploy TACo Application + run: ape run scripts/deploy_taco_application.py --network ethereum:local - name: Deploy Staking Escrow run: ape run scripts/deploy_staking_escrow.py --network ethereum:local diff --git a/ape-config.yaml b/ape-config.yaml index 5dd4d21c..2a9c00c2 100644 --- a/ape-config.yaml +++ b/ape-config.yaml @@ -18,6 +18,9 @@ dependencies: - name: fx-portal github: 0xPolygon/fx-portal version: 1.0.5 + - name: threshold + github: threshold-network/solidity-contracts + version: 1.2.1 solidity: version: 0.8.20 @@ -26,6 +29,7 @@ solidity: - "@openzeppelin/contracts=openzeppelin/v4.9.1" - "@openzeppelin-upgradeable/contracts=openzeppelin-upgradeable/v4.9.1" - "@fx-portal/contracts=fx-portal/v1.0.5" + - "@threshold/contracts=threshold/v1.2.1" deployments: polygon: @@ -44,6 +48,14 @@ deployments: - nu_token_supply: 1_000_000_000 pre_min_authorization: 40000000000000000000000 pre_min_operator_seconds: 86400 # one day in seconds + pre_hash_algorithm: 1 + pre_base_penalty: 2 + pre_penalty_history_coefficient: 0 + pre_percentage_penalty_coefficient: 100000 + pre_min_authorization: 40000000000000000000000 + pre_min_operator_seconds: 86400 # one day in seconds + reward_duration: 604800 # one week in seconds + deauthorization_duration: 5184000 # 60 days in seconds verify: False rinkeby: - nu_token: '0x78D591D90a4a768B9D2790deA465D472b6Fe0f18' diff --git a/contracts/contracts/Adjudicator.sol b/contracts/contracts/Adjudicator.sol index 7acfe8cd..a2be80c9 100644 --- a/contracts/contracts/Adjudicator.sol +++ b/contracts/contracts/Adjudicator.sol @@ -4,19 +4,20 @@ pragma solidity ^0.8.0; import "./lib/ReEncryptionValidator.sol"; import "./lib/SignatureVerifier.sol"; -import "./IStakingEscrow.sol"; -import "./proxy/Upgradeable.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "./TACoApplication.sol"; /** * @title Adjudicator -* @notice Supervises stakers' behavior and punishes when something's wrong. -* @dev |v2.1.2| +* @notice Supervises operators' behavior and punishes when something's wrong. +* @dev |v3.1.1| */ -contract Adjudicator is Upgradeable { +contract Adjudicator { using UmbralDeserializer for bytes; + using SafeCast for uint256; event CFragEvaluated( bytes32 indexed evaluationHash, @@ -25,62 +26,57 @@ contract Adjudicator is Upgradeable { ); event IncorrectCFragVerdict( bytes32 indexed evaluationHash, - address indexed worker, - address indexed staker + address indexed operator, + address indexed stakingProvider ); // used only for upgrading bytes32 constant RESERVED_CAPSULE_AND_CFRAG_BYTES = bytes32(0); address constant RESERVED_ADDRESS = address(0); - IStakingEscrow public immutable escrow; SignatureVerifier.HashAlgorithm public immutable hashAlgorithm; uint256 public immutable basePenalty; uint256 public immutable penaltyHistoryCoefficient; uint256 public immutable percentagePenaltyCoefficient; - uint256 public immutable rewardCoefficient; + TACoApplication public immutable application; mapping (address => uint256) public penaltyHistory; mapping (bytes32 => bool) public evaluatedCFrags; /** - * @param _escrow Escrow contract * @param _hashAlgorithm Hashing algorithm * @param _basePenalty Base for the penalty calculation * @param _penaltyHistoryCoefficient Coefficient for calculating the penalty depending on the history * @param _percentagePenaltyCoefficient Coefficient for calculating the percentage penalty - * @param _rewardCoefficient Coefficient for calculating the reward */ constructor( - IStakingEscrow _escrow, + TACoApplication _application, SignatureVerifier.HashAlgorithm _hashAlgorithm, uint256 _basePenalty, uint256 _penaltyHistoryCoefficient, - uint256 _percentagePenaltyCoefficient, - uint256 _rewardCoefficient + uint256 _percentagePenaltyCoefficient ) { - // Sanity checks. - require(_escrow.secondsPerPeriod() > 0 && // This contract has an escrow, and it's not the null address. - // The reward and penalty coefficients are set. - _percentagePenaltyCoefficient != 0 && - _rewardCoefficient != 0); - escrow = _escrow; + require( + _percentagePenaltyCoefficient != 0 && + address(_application.token()) != address(0), + "Wrong input parameters" + ); hashAlgorithm = _hashAlgorithm; basePenalty = _basePenalty; percentagePenaltyCoefficient = _percentagePenaltyCoefficient; penaltyHistoryCoefficient = _penaltyHistoryCoefficient; - rewardCoefficient = _rewardCoefficient; + application = _application; } /** - * @notice Submit proof that a worker created wrong CFrag + * @notice Submit proof that a operator created wrong CFrag * @param _capsuleBytes Serialized capsule * @param _cFragBytes Serialized CFrag - * @param _cFragSignature Signature of CFrag by worker + * @param _cFragSignature Signature of CFrag by operator * @param _taskSignature Signature of task specification by Bob * @param _requesterPublicKey Bob's signing public key, also known as "stamp" - * @param _workerPublicKey Worker's signing public key, also known as "stamp" - * @param _workerIdentityEvidence Signature of worker's public key by worker's eth-key + * @param _operatorPublicKey Operator's signing public key, also known as "stamp" + * @param _operatorIdentityEvidence Signature of operator's public key by operator's eth-key * @param _preComputedData Additional pre-computed data for CFrag correctness verification */ function evaluateCFrag( @@ -89,8 +85,8 @@ contract Adjudicator is Upgradeable { bytes memory _cFragSignature, bytes memory _taskSignature, bytes memory _requesterPublicKey, - bytes memory _workerPublicKey, - bytes memory _workerIdentityEvidence, + bytes memory _operatorPublicKey, + bytes memory _operatorIdentityEvidence, bytes memory _preComputedData ) public @@ -106,28 +102,28 @@ contract Adjudicator is Upgradeable { emit CFragEvaluated(evaluationHash, msg.sender, cFragIsCorrect); // 3. Verify associated public keys and signatures - require(ReEncryptionValidator.checkSerializedCoordinates(_workerPublicKey), + require(ReEncryptionValidator.checkSerializedCoordinates(_operatorPublicKey), "Staker's public key is invalid"); require(ReEncryptionValidator.checkSerializedCoordinates(_requesterPublicKey), "Requester's public key is invalid"); UmbralDeserializer.PreComputedData memory precomp = _preComputedData.toPreComputedData(); - // Verify worker's signature of CFrag + // Verify operator's signature of CFrag require(SignatureVerifier.verify( _cFragBytes, abi.encodePacked(_cFragSignature, precomp.lostBytes[1]), - _workerPublicKey, + _operatorPublicKey, hashAlgorithm), "CFrag signature is invalid" ); - // Verify worker's signature of taskSignature and that it corresponds to cfrag.proof.metadata + // Verify operator's signature of taskSignature and that it corresponds to cfrag.proof.metadata UmbralDeserializer.CapsuleFrag memory cFrag = _cFragBytes.toCapsuleFrag(); require(SignatureVerifier.verify( _taskSignature, abi.encodePacked(cFrag.proof.metadata, precomp.lostBytes[2]), - _workerPublicKey, + _operatorPublicKey, hashAlgorithm), "Task signature is invalid" ); @@ -136,14 +132,14 @@ contract Adjudicator is Upgradeable { // A task specification is: capsule + ursula pubkey + alice address + blockhash bytes32 stampXCoord; assembly { - stampXCoord := mload(add(_workerPublicKey, 32)) + stampXCoord := mload(add(_operatorPublicKey, 32)) } bytes memory stamp = abi.encodePacked(precomp.lostBytes[4], stampXCoord); require(SignatureVerifier.verify( abi.encodePacked(_capsuleBytes, stamp, - _workerIdentityEvidence, + _operatorIdentityEvidence, precomp.alicesKeyAsAddress, bytes32(0)), abi.encodePacked(_taskSignature, precomp.lostBytes[3]), @@ -152,58 +148,38 @@ contract Adjudicator is Upgradeable { "Specification signature is invalid" ); - // 4. Extract worker address from stamp signature. - address worker = SignatureVerifier.recover( + // 4. Extract operator address from stamp signature. + address operator = SignatureVerifier.recover( SignatureVerifier.hashEIP191(stamp, bytes1(0x45)), // Currently, we use version E (0x45) of EIP191 signatures - _workerIdentityEvidence); - address staker = escrow.stakerFromWorker(worker); - require(staker != address(0), "Worker must be related to a staker"); + _operatorIdentityEvidence); + address stakingProvider = application.stakingProviderFromOperator(operator); + require(stakingProvider != address(0), "Operator must be associated with a provider"); - // 5. Check that staker can be slashed - uint256 stakerValue = escrow.getAllTokens(staker); - require(stakerValue > 0, "Staker has no tokens"); + // 5. Check that staking provider can be slashed + uint96 stakingProviderValue = application.authorizedStake(stakingProvider); + require(stakingProviderValue > 0, "Provider has no tokens"); - // 6. If CFrag was incorrect, slash staker + // 6. If CFrag was incorrect, slash staking provider if (!cFragIsCorrect) { - (uint256 penalty, uint256 reward) = calculatePenaltyAndReward(staker, stakerValue); - escrow.slashStaker(staker, penalty, msg.sender, reward); - emit IncorrectCFragVerdict(evaluationHash, worker, staker); + uint96 penalty = calculatePenalty(stakingProvider, stakingProviderValue); + application.slash(stakingProvider, penalty, msg.sender); + emit IncorrectCFragVerdict(evaluationHash, operator, stakingProvider); } } /** - * @notice Calculate penalty to the staker and reward to the investigator - * @param _staker Staker's address - * @param _stakerValue Amount of tokens that belong to the staker + * @notice Calculate penalty to the staking provider + * @param _stakingProvider Staking provider address + * @param _stakingProviderValue Amount of tokens that belong to the staking provider */ - function calculatePenaltyAndReward(address _staker, uint256 _stakerValue) - internal returns (uint256 penalty, uint256 reward) + function calculatePenalty(address _stakingProvider, uint96 _stakingProviderValue) + internal returns (uint96) { - penalty = basePenalty + penaltyHistoryCoefficient * penaltyHistory[_staker]; - penalty = Math.min(penalty, _stakerValue / percentagePenaltyCoefficient); - reward = penalty / rewardCoefficient; + uint256 penalty = basePenalty + penaltyHistoryCoefficient * penaltyHistory[_stakingProvider]; + penalty = Math.min(penalty, _stakingProviderValue / percentagePenaltyCoefficient); // TODO add maximum condition or other overflow protection or other penalty condition (#305?) - penaltyHistory[_staker] = penaltyHistory[_staker] + 1; - } - - /// @dev the `onlyWhileUpgrading` modifier works through a call to the parent `verifyState` - function verifyState(address _testTarget) public override virtual { - super.verifyState(_testTarget); - bytes32 evaluationCFragHash = SignatureVerifier.hash( - abi.encodePacked(RESERVED_CAPSULE_AND_CFRAG_BYTES), SignatureVerifier.HashAlgorithm.SHA256); - require(delegateGet(_testTarget, this.evaluatedCFrags.selector, evaluationCFragHash) == - (evaluatedCFrags[evaluationCFragHash] ? 1 : 0)); - require(delegateGet(_testTarget, this.penaltyHistory.selector, bytes32(bytes20(RESERVED_ADDRESS))) == - penaltyHistory[RESERVED_ADDRESS]); + penaltyHistory[_stakingProvider] = penaltyHistory[_stakingProvider] + 1; + return penalty.toUint96(); } - /// @dev the `onlyWhileUpgrading` modifier works through a call to the parent `finishUpgrade` - function finishUpgrade(address _target) public override virtual { - super.finishUpgrade(_target); - // preparation for the verifyState method - bytes32 evaluationCFragHash = SignatureVerifier.hash( - abi.encodePacked(RESERVED_CAPSULE_AND_CFRAG_BYTES), SignatureVerifier.HashAlgorithm.SHA256); - evaluatedCFrags[evaluationCFragHash] = true; - penaltyHistory[RESERVED_ADDRESS] = 123; - } } diff --git a/contracts/contracts/SimplePREApplication.sol b/contracts/contracts/SimplePREApplication.sol deleted file mode 100644 index f43d901c..00000000 --- a/contracts/contracts/SimplePREApplication.sol +++ /dev/null @@ -1,226 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -pragma solidity ^0.8.0; - - -import "../threshold/IStaking.sol"; - - -/** -* @title PRE Application -* @notice Contract handles PRE configuration -*/ -contract SimplePREApplication { - - /** - * @notice Signals that an operator was bonded to the staking provider - * @param stakingProvider Staking provider address - * @param operator Operator address - * @param startTimestamp Timestamp bonding occurred - */ - event OperatorBonded(address indexed stakingProvider, address indexed operator, uint256 startTimestamp); - - /** - * @notice Signals that an operator address is confirmed - * @param stakingProvider Staking provider address - * @param operator Operator address - */ - event OperatorConfirmed(address indexed stakingProvider, address indexed operator); - - struct StakingProviderInfo { - address operator; - bool operatorConfirmed; - uint256 operatorStartTimestamp; - } - - uint256 public immutable minAuthorization; - uint256 public immutable minOperatorSeconds; - - IStaking public immutable tStaking; - - mapping (address => StakingProviderInfo) public stakingProviderInfo; - address[] public stakingProviders; - mapping(address => address) internal _stakingProviderFromOperator; - - - /** - * @notice Constructor sets address of token contract and parameters for staking - * @param _tStaking T token staking contract - * @param _minAuthorization Amount of minimum allowable authorization - * @param _minOperatorSeconds Min amount of seconds while an operator can't be changed - */ - constructor( - IStaking _tStaking, - uint256 _minAuthorization, - uint256 _minOperatorSeconds - ) { - require( - _tStaking.authorizedStake(address(this), address(this)) == 0, - "Wrong input parameters" - ); - minAuthorization = _minAuthorization; - tStaking = _tStaking; - minOperatorSeconds = _minOperatorSeconds; - } - - /** - * @dev Checks caller is a staking provider or stake owner - */ - modifier onlyOwnerOrStakingProvider(address _stakingProvider) - { - require(isAuthorized(_stakingProvider), "Not owner or provider"); - if (_stakingProvider != msg.sender) { - (address owner,,) = tStaking.rolesOf(_stakingProvider); - require(owner == msg.sender, "Not owner or provider"); - } - _; - } - - - //-------------------------Main------------------------- - /** - * @notice Returns staking provider for specified operator - */ - function stakingProviderFromOperator(address _operator) public view returns (address) { - return _stakingProviderFromOperator[_operator]; - } - - /** - * @notice Returns operator for specified staking provider - */ - function getOperatorFromStakingProvider(address _stakingProvider) public view returns (address) { - return stakingProviderInfo[_stakingProvider].operator; - } - - /** - * @notice Get all tokens delegated to the staking provider - */ - function authorizedStake(address _stakingProvider) public view returns (uint96) { - (uint96 tStake, uint96 keepInTStake, uint96 nuInTStake) = tStaking.stakes(_stakingProvider); - return tStake + keepInTStake + nuInTStake; - } - - /** - * @notice Get the value of authorized tokens for active providers as well as providers and their authorized tokens - * @param _startIndex Start index for looking in providers array - * @param _maxStakingProviders Max providers for looking, if set 0 then all will be used - * @return allAuthorizedTokens Sum of authorized tokens for active providers - * @return activeStakingProviders Array of providers and their authorized tokens. - * Providers addresses stored as uint256 - * @dev Note that activeStakingProviders[0] is an array of uint256, but you want addresses. - * Careful when used directly! - */ - function getActiveStakingProviders(uint256 _startIndex, uint256 _maxStakingProviders) - external view returns (uint256 allAuthorizedTokens, uint256[2][] memory activeStakingProviders) - { - uint256 endIndex = stakingProviders.length; - require(_startIndex < endIndex, "Wrong start index"); - if (_maxStakingProviders != 0 && _startIndex + _maxStakingProviders < endIndex) { - endIndex = _startIndex + _maxStakingProviders; - } - activeStakingProviders = new uint256[2][](endIndex - _startIndex); - allAuthorizedTokens = 0; - - uint256 resultIndex = 0; - for (uint256 i = _startIndex; i < endIndex; i++) { - address stakingProvider = stakingProviders[i]; - StakingProviderInfo storage info = stakingProviderInfo[stakingProvider]; - uint256 eligibleAmount = authorizedStake(stakingProvider); - if (eligibleAmount < minAuthorization || !info.operatorConfirmed) { - continue; - } - activeStakingProviders[resultIndex][0] = uint256(uint160(stakingProvider)); - activeStakingProviders[resultIndex++][1] = eligibleAmount; - allAuthorizedTokens += eligibleAmount; - } - assembly { - mstore(activeStakingProviders, resultIndex) - } - } - - /** - * @notice Returns beneficiary related to the staking provider - */ - function getBeneficiary(address _stakingProvider) public view returns (address payable beneficiary) { - (, beneficiary,) = tStaking.rolesOf(_stakingProvider); - } - - /** - * @notice Returns true if staking provider has authorized stake to this application - */ - function isAuthorized(address _stakingProvider) public view returns (bool) { - return authorizedStake(_stakingProvider) >= minAuthorization; - } - - /** - * @notice Returns true if operator has confirmed address - */ - // TODO maybe _stakingProvider instead of _operator as input? - function isOperatorConfirmed(address _operator) public view returns (bool) { - address stakingProvider = _stakingProviderFromOperator[_operator]; - StakingProviderInfo storage info = stakingProviderInfo[stakingProvider]; - return info.operatorConfirmed; - } - - /** - * @notice Return the length of the array of staking providers - */ - function getStakingProvidersLength() external view returns (uint256) { - return stakingProviders.length; - } - - /** - * @notice Bond operator - * @param _stakingProvider Staking provider address - * @param _operator Operator address. Must be a real address, not a contract - */ - function bondOperator(address _stakingProvider, address _operator) - external onlyOwnerOrStakingProvider(_stakingProvider) - { - StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; - require(_operator != info.operator, "Specified operator is already bonded with this provider"); - // If this staker had an operator ... - if (info.operator != address(0)) { - require( - block.timestamp >= info.operatorStartTimestamp + minOperatorSeconds, - "Not enough time passed to change operator" - ); - // Remove the old relation "operator->stakingProvider" - _stakingProviderFromOperator[info.operator] = address(0); - } - - if (_operator != address(0)) { - require(_stakingProviderFromOperator[_operator] == address(0), "Specified operator is already in use"); - require( - _operator == _stakingProvider || getBeneficiary(_operator) == address(0), - "Specified operator is a provider" - ); - // Set new operator->stakingProvider relation - _stakingProviderFromOperator[_operator] = _stakingProvider; - } - - if (info.operatorStartTimestamp == 0) { - stakingProviders.push(_stakingProvider); - } - - // Bond new operator (or unbond if _operator == address(0)) - info.operator = _operator; - info.operatorStartTimestamp = block.timestamp; - info.operatorConfirmed = false; - emit OperatorBonded(_stakingProvider, _operator, block.timestamp); - } - - /** - * @notice Make a confirmation by operator - */ - function confirmOperatorAddress() external { - address stakingProvider = _stakingProviderFromOperator[msg.sender]; - require(isAuthorized(stakingProvider), "No stake associated with the operator"); - StakingProviderInfo storage info = stakingProviderInfo[stakingProvider]; - require(!info.operatorConfirmed, "Operator address is already confirmed"); - require(msg.sender == tx.origin, "Only operator with real address can make a confirmation"); - info.operatorConfirmed = true; - emit OperatorConfirmed(stakingProvider, msg.sender); - } - -} diff --git a/contracts/contracts/StakingEscrow.sol b/contracts/contracts/StakingEscrow.sol index 87d15ccd..aa9ec8c5 100644 --- a/contracts/contracts/StakingEscrow.sol +++ b/contracts/contracts/StakingEscrow.sol @@ -9,7 +9,7 @@ import "./lib/Bits.sol"; import "./proxy/Upgradeable.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "../threshold/IStaking.sol"; +import "@threshold/contracts/staking/IStaking.sol"; /** diff --git a/contracts/contracts/TACoApplication.sol b/contracts/contracts/TACoApplication.sol new file mode 100644 index 00000000..952fde43 --- /dev/null +++ b/contracts/contracts/TACoApplication.sol @@ -0,0 +1,708 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.0; + + +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import "@threshold/contracts/staking/IApplication.sol"; +import "@threshold/contracts/staking/IStaking.sol"; +import "./coordination/IUpdatableStakeInfo.sol"; + + +/** +* @title TACo Application +* @notice Contract distributes rewards for participating in app and slashes for violating rules +*/ +contract TACoApplication is IApplication, OwnableUpgradeable { + + using SafeERC20 for IERC20; + using SafeCast for uint256; + + /** + * @notice Signals that distributor role was set + * @param distributor Address of reward distributor + */ + event RewardDistributorSet(address indexed distributor); + + /** + * @notice Signals that reward was added + * @param reward Amount of reward + */ + event RewardAdded(uint256 reward); + + /** + * @notice Signals that the beneficiary related to the staking provider received reward + * @param stakingProvider Staking provider address + * @param beneficiary Beneficiary address + * @param reward Amount of reward + */ + event RewardPaid(address indexed stakingProvider, address indexed beneficiary, uint256 reward); + + /** + * @notice Signals that authorization was increased for the staking provider + * @param stakingProvider Staking provider address + * @param fromAmount Previous amount of increased authorization + * @param toAmount New amount of increased authorization + */ + event AuthorizationIncreased(address indexed stakingProvider, uint96 fromAmount, uint96 toAmount); + + /** + * @notice Signals that authorization was decreased involuntary + * @param stakingProvider Staking provider address + * @param fromAmount Previous amount of authorized tokens + * @param toAmount Amount of authorized tokens to decrease + */ + event AuthorizationInvoluntaryDecreased(address indexed stakingProvider, uint96 fromAmount, uint96 toAmount); + + /** + * @notice Signals that authorization decrease was requested for the staking provider + * @param stakingProvider Staking provider address + * @param fromAmount Current amount of authorized tokens + * @param toAmount Amount of authorization to decrease + */ + event AuthorizationDecreaseRequested(address indexed stakingProvider, uint96 fromAmount, uint96 toAmount); + + /** + * @notice Signals that authorization decrease was approved for the staking provider + * @param stakingProvider Staking provider address + * @param fromAmount Previous amount of authorized tokens + * @param toAmount Decreased amount of authorized tokens + */ + event AuthorizationDecreaseApproved(address indexed stakingProvider, uint96 fromAmount, uint96 toAmount); + + /** + * @notice Signals that authorization was resynchronized + * @param stakingProvider Staking provider address + * @param fromAmount Previous amount of authorized tokens + * @param toAmount Resynchronized amount of authorized tokens + */ + event AuthorizationReSynchronized(address indexed stakingProvider, uint96 fromAmount, uint96 toAmount); + + /** + * @notice Signals that the staking provider was slashed + * @param stakingProvider Staking provider address + * @param penalty Slashing penalty + * @param investigator Investigator address + * @param reward Value of reward provided to investigator (in units of T) + */ + event Slashed(address indexed stakingProvider, uint256 penalty, address indexed investigator, uint256 reward); + + /** + * @notice Signals that an operator was bonded to the staking provider + * @param stakingProvider Staking provider address + * @param operator Operator address + * @param previousOperator Previous operator address + * @param startTimestamp Timestamp bonding occurred + */ + event OperatorBonded( + address indexed stakingProvider, + address indexed operator, + address indexed previousOperator, + uint256 startTimestamp + ); + + /** + * @notice Signals that an operator address is confirmed + * @param stakingProvider Staking provider address + * @param operator Operator address + */ + event OperatorConfirmed(address indexed stakingProvider, address indexed operator); + + struct StakingProviderInfo { + address operator; + bool operatorConfirmed; + uint64 operatorStartTimestamp; + + uint96 authorized; + uint96 deauthorizing; // TODO real usage only in getActiveStakingProviders, maybe remove? + uint64 endDeauthorization; + + uint96 tReward; + uint96 rewardPerTokenPaid; + } + + uint96 public immutable minimumAuthorization; + uint256 public immutable minOperatorSeconds; + uint256 public immutable rewardDuration; + uint256 public immutable deauthorizationDuration; + + IStaking public immutable tStaking; + IERC20 public immutable token; + + IUpdatableStakeInfo public updatableStakeInfo; + address public adjudicator; + + mapping (address => StakingProviderInfo) public stakingProviderInfo; + address[] public stakingProviders; + mapping(address => address) internal _stakingProviderFromOperator; + + address public rewardDistributor; + uint256 public periodFinish; + uint256 public rewardRateDecimals; + uint256 public lastUpdateTime; + uint96 public rewardPerTokenStored; + uint96 public authorizedOverall; + + /** + * @notice Constructor sets address of token contract and parameters for staking + * @param _token T token contract + * @param _tStaking T token staking contract + * @param _minimumAuthorization Amount of minimum allowable authorization + * @param _minOperatorSeconds Min amount of seconds while an operator can't be changed + * @param _rewardDuration Duration of one reward cycle in seconds + * @param _deauthorizationDuration Duration of decreasing authorization in seconds + */ + constructor( + IERC20 _token, + IStaking _tStaking, + uint96 _minimumAuthorization, + uint256 _minOperatorSeconds, + uint256 _rewardDuration, + uint256 _deauthorizationDuration + ) { + require( + _rewardDuration != 0 && + _tStaking.authorizedStake(address(this), address(this)) == 0 && + _token.totalSupply() > 0, + "Wrong input parameters" + ); + rewardDuration = _rewardDuration; + deauthorizationDuration = _deauthorizationDuration; + minimumAuthorization = _minimumAuthorization; + token = _token; + tStaking = _tStaking; + minOperatorSeconds = _minOperatorSeconds; + _disableInitializers(); + } + + /** + * @dev Update reward for the specified staking provider + */ + modifier updateReward(address _stakingProvider) { + updateRewardInternal(_stakingProvider); + _; + } + + /** + * @dev Checks caller is T staking contract + */ + modifier onlyStakingContract() + { + require(msg.sender == address(tStaking), "Caller must be the T staking contract"); + _; + } + + /** + * @dev Checks caller is a staking provider or stake owner + */ + modifier onlyOwnerOrStakingProvider(address _stakingProvider) + { + require(isAuthorized(_stakingProvider), "Not owner or provider"); + if (_stakingProvider != msg.sender) { + (address owner,,) = tStaking.rolesOf(_stakingProvider); + require(owner == msg.sender, "Not owner or provider"); + } + _; + } + + /** + * @notice Initialize function for using with OpenZeppelin proxy + */ + function initialize() external initializer { + __Ownable_init(); + } + + /** + * @notice Set contract for multi-chain interactions + */ + function setUpdatableStakeInfo(IUpdatableStakeInfo _updatableStakeInfo) external onlyOwner { + require(address(_updatableStakeInfo) != address(updatableStakeInfo), "New address must not be equal to the current one"); + if (address(_updatableStakeInfo) != address(0)) { + // trying to call contract to be sure that is correct address + _updatableStakeInfo.updateOperator(address(0), address(0)); + } + updatableStakeInfo = _updatableStakeInfo; + } + + /** + * @notice Set adjudicator contract. If zero then slashing is disabled + */ + function setAdjudicator(address _adjudicator) external onlyOwner { + require(address(_adjudicator) != address(adjudicator), "New address must not be equal to the current one"); + adjudicator = _adjudicator; + } + + //------------------------Reward------------------------------ + + /** + * @notice Set reward distributor address + */ + function setRewardDistributor(address _rewardDistributor) + external + onlyOwner + { + rewardDistributor = _rewardDistributor; + emit RewardDistributorSet(_rewardDistributor); + } + + /** + * @notice Update reward for the specified staking provider + * @param _stakingProvider Staking provider address + */ + function updateRewardInternal(address _stakingProvider) internal { + rewardPerTokenStored = rewardPerToken(); + lastUpdateTime = lastTimeRewardApplicable(); + if (_stakingProvider != address(0)) { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + info.tReward = availableRewards(_stakingProvider); + info.rewardPerTokenPaid = rewardPerTokenStored; + } + } + + /** + * @notice Returns last time when reward was applicable + */ + function lastTimeRewardApplicable() public view returns (uint256) { + return Math.min(block.timestamp, periodFinish); + } + + /** + * @notice Returns current value of reward per token + */ + function rewardPerToken() public view returns (uint96) { + if (authorizedOverall == 0) { + return rewardPerTokenStored; + } + uint256 result = rewardPerTokenStored + + (lastTimeRewardApplicable() - lastUpdateTime) + * rewardRateDecimals + / authorizedOverall; + return result.toUint96(); + } + + /** + * @notice Returns amount of reward in T units for the staking provider + * @param _stakingProvider Staking provider address + */ + function availableRewards(address _stakingProvider) public view returns (uint96) { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + if (!info.operatorConfirmed) { + return info.tReward; + } + uint256 result = uint256(info.authorized) * + (rewardPerToken() - info.rewardPerTokenPaid) + / 1e18 + + info.tReward; + return result.toUint96(); + } + + /** + * @notice Transfer reward for the next period. Can be called only by distributor + * @param _reward Amount of reward + */ + function pushReward(uint96 _reward) external updateReward(address(0)) { + require(msg.sender == rewardDistributor, "Only distributor can push rewards"); + require(_reward > 0, "Reward must be specified"); + if (block.timestamp >= periodFinish) { + rewardRateDecimals = uint256(_reward) * 1e18 / rewardDuration; + } else { + uint256 remaining = periodFinish - block.timestamp; + uint256 leftover = remaining * rewardRateDecimals; + rewardRateDecimals = (uint256(_reward) * 1e18 + leftover) / rewardDuration; + } + lastUpdateTime = block.timestamp; + periodFinish = block.timestamp + rewardDuration; + emit RewardAdded(_reward); + token.safeTransferFrom(msg.sender, address(this), _reward); + } + + /** + * @notice Withdraw available amount of T reward to beneficiary. Can be called only by beneficiary + * @param _stakingProvider Staking provider address + */ + function withdrawRewards(address _stakingProvider) external updateReward(_stakingProvider) { + address beneficiary = getBeneficiary(_stakingProvider); + require(msg.sender == beneficiary, "Caller must be beneficiary"); + + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + uint96 value = info.tReward; + require(value > 0, "No reward to withdraw"); + info.tReward = 0; + emit RewardPaid(_stakingProvider, beneficiary, value); + token.safeTransfer(beneficiary, value); + } + + //------------------------Authorization------------------------------ + /** + * @notice Recalculate `authorizedOverall` if desync happened + */ + function resynchronizeAuthorizedOverall(StakingProviderInfo storage _info, uint96 _properAmount) internal { + if (_info.authorized != _properAmount) { + authorizedOverall -= _info.authorized - _properAmount; + } + } + + /** + * @notice Recalculate reward and save increased authorization. Can be called only by staking contract + * @param _stakingProvider Address of staking provider + * @param _fromAmount Amount of previously authorized tokens to TACo application by staking provider + * @param _toAmount Amount of authorized tokens to TACo application by staking provider + */ + function authorizationIncreased( + address _stakingProvider, + uint96 _fromAmount, + uint96 _toAmount + ) + external override onlyStakingContract updateReward(_stakingProvider) + { + require(_stakingProvider != address(0) && _toAmount > 0, "Input parameters must be specified"); + require(_toAmount >= minimumAuthorization, "Authorization must be greater than minimum"); + + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + require( + _stakingProviderFromOperator[_stakingProvider] == address(0) || + _stakingProviderFromOperator[_stakingProvider] == _stakingProvider, + "A provider can't be an operator for another provider" + ); + + if (info.operatorConfirmed) { + resynchronizeAuthorizedOverall(info, _fromAmount); + authorizedOverall += _toAmount - _fromAmount; + } + + info.authorized = _toAmount; + emit AuthorizationIncreased(_stakingProvider, _fromAmount, _toAmount); + _updateAuthorization(_stakingProvider, info); + } + + /** + * @notice Immediately decrease authorization. Can be called only by staking contract + * @param _stakingProvider Address of staking provider + * @param _fromAmount Previous amount of authorized tokens + * @param _toAmount Amount of authorized tokens to decrease + */ + function involuntaryAuthorizationDecrease( + address _stakingProvider, + uint96 _fromAmount, + uint96 _toAmount + ) + external override onlyStakingContract updateReward(_stakingProvider) + { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + if (info.operatorConfirmed) { + resynchronizeAuthorizedOverall(info, _fromAmount); + authorizedOverall -= _fromAmount - _toAmount; + } + + info.authorized = _toAmount; + if (info.authorized < info.deauthorizing) { + info.deauthorizing = info.authorized; + } + emit AuthorizationInvoluntaryDecreased(_stakingProvider, _fromAmount, _toAmount); + + if (info.authorized == 0) { + _stakingProviderFromOperator[info.operator] = address(0); + info.operator = address(0); + _releaseOperator(_stakingProvider); + } + _updateAuthorization(_stakingProvider, info); + } + + /** + * @notice Register request of decreasing authorization. Can be called only by staking contract + * @param _stakingProvider Address of staking provider + * @param _fromAmount Current amount of authorized tokens + * @param _toAmount Amount of authorized tokens to decrease + */ + function authorizationDecreaseRequested( + address _stakingProvider, + uint96 _fromAmount, + uint96 _toAmount + ) + external override onlyStakingContract updateReward(_stakingProvider) + { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + require(_toAmount <= info.authorized, "Amount to decrease greater than authorized"); + require( + _toAmount == 0 || _toAmount >= minimumAuthorization, + "Resulting authorization will be less than minimum" + ); + if (info.operatorConfirmed) { + resynchronizeAuthorizedOverall(info, _fromAmount); + } + + info.authorized = _fromAmount; + info.deauthorizing = _fromAmount - _toAmount; + info.endDeauthorization = uint64(block.timestamp + deauthorizationDuration); + emit AuthorizationDecreaseRequested(_stakingProvider, _fromAmount, _toAmount); + _updateAuthorization(_stakingProvider, info); + } + + /** + * @notice Approve request of decreasing authorization. Can be called by anyone + * @param _stakingProvider Address of staking provider + */ + function finishAuthorizationDecrease(address _stakingProvider) external updateReward(_stakingProvider) { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + require(info.deauthorizing > 0, "There is no deauthorizing in process"); + require(info.endDeauthorization <= block.timestamp, "Authorization decrease has not finished yet"); + + uint96 toAmount = tStaking.approveAuthorizationDecrease(_stakingProvider); + + if (info.operatorConfirmed) { + authorizedOverall -= info.authorized - toAmount; + } + + emit AuthorizationDecreaseApproved(_stakingProvider, info.authorized, toAmount); + info.authorized = toAmount; + info.deauthorizing = 0; + info.endDeauthorization = 0; + + if (info.authorized == 0) { + _stakingProviderFromOperator[info.operator] = address(0); + info.operator = address(0); + _releaseOperator(_stakingProvider); + } + _updateAuthorization(_stakingProvider, info); + } + + /** + * @notice Read authorization from staking contract and store it. Can be called by anyone + * @param _stakingProvider Address of staking provider + */ + function resynchronizeAuthorization(address _stakingProvider) external updateReward(_stakingProvider) { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + uint96 newAuthorized = tStaking.authorizedStake(_stakingProvider, address(this)); + require(info.authorized > newAuthorized, "Nothing to synchronize"); + + if (info.operatorConfirmed) { + authorizedOverall -= info.authorized - newAuthorized; + } + emit AuthorizationReSynchronized(_stakingProvider, info.authorized, newAuthorized); + + info.authorized = newAuthorized; + if (info.authorized < info.deauthorizing) { + info.deauthorizing = info.authorized; + } + + if (info.authorized == 0) { + _stakingProviderFromOperator[info.operator] = address(0); + info.operator = address(0); + _releaseOperator(_stakingProvider); + } + _updateAuthorization(_stakingProvider, info); + } + + //-------------------------Main------------------------- + /** + * @notice Returns staking provider for specified operator + */ + function stakingProviderFromOperator(address _operator) public view returns (address) { + return _stakingProviderFromOperator[_operator]; + } + + /** + * @notice Returns operator for specified staking provider + */ + function getOperatorFromStakingProvider(address _stakingProvider) public view returns (address) { + return stakingProviderInfo[_stakingProvider].operator; + } + + /** + * @notice Get all tokens delegated to the staking provider + */ + function authorizedStake(address _stakingProvider) public view returns (uint96) { + return stakingProviderInfo[_stakingProvider].authorized; + } + + /** + * @notice Get all tokens delegated to the staking provider + */ + function getEligibleAmount(StakingProviderInfo storage _info) internal view returns (uint96) { + return _info.authorized - _info.deauthorizing; + } + + /** + * @notice Get the value of authorized tokens for active providers as well as providers and their authorized tokens + * @param _startIndex Start index for looking in providers array + * @param _maxStakingProviders Max providers for looking, if set 0 then all will be used + * @return allAuthorizedTokens Sum of authorized tokens for active providers + * @return activeStakingProviders Array of providers and their authorized tokens. + * Providers addresses stored as uint256 + * @dev Note that activeStakingProviders[0] is an array of uint256, but you want addresses. + * Careful when used directly! + */ + function getActiveStakingProviders(uint256 _startIndex, uint256 _maxStakingProviders) + external view returns (uint256 allAuthorizedTokens, uint256[2][] memory activeStakingProviders) + { + uint256 endIndex = stakingProviders.length; + require(_startIndex < endIndex, "Wrong start index"); + if (_maxStakingProviders != 0 && _startIndex + _maxStakingProviders < endIndex) { + endIndex = _startIndex + _maxStakingProviders; + } + activeStakingProviders = new uint256[2][](endIndex - _startIndex); + allAuthorizedTokens = 0; + + uint256 resultIndex = 0; + for (uint256 i = _startIndex; i < endIndex; i++) { + address stakingProvider = stakingProviders[i]; + StakingProviderInfo storage info = stakingProviderInfo[stakingProvider]; + uint256 eligibleAmount = getEligibleAmount(info); + if (eligibleAmount < minimumAuthorization || !info.operatorConfirmed) { + continue; + } + activeStakingProviders[resultIndex][0] = uint256(uint160(stakingProvider)); + activeStakingProviders[resultIndex++][1] = eligibleAmount; + allAuthorizedTokens += eligibleAmount; + } + assembly { + mstore(activeStakingProviders, resultIndex) + } + } + + /** + * @notice Returns beneficiary related to the staking provider + */ + function getBeneficiary(address _stakingProvider) public view returns (address payable beneficiary) { + (, beneficiary,) = tStaking.rolesOf(_stakingProvider); + } + + /** + * @notice Returns true if staking provider has authorized stake to this application + */ + function isAuthorized(address _stakingProvider) public view returns (bool) { + return stakingProviderInfo[_stakingProvider].authorized > 0; + } + + /** + * @notice Returns true if operator has confirmed address + */ + // TODO maybe _stakingProvider instead of _operator as input? + function isOperatorConfirmed(address _operator) public view returns (bool) { + address stakingProvider = _stakingProviderFromOperator[_operator]; + StakingProviderInfo storage info = stakingProviderInfo[stakingProvider]; + return info.operatorConfirmed; + } + + /** + * @notice Return the length of the array of staking providers + */ + function getStakingProvidersLength() external view returns (uint256) { + return stakingProviders.length; + } + + /** + * @notice Bond operator + * @param _stakingProvider Staking provider address + * @param _operator Operator address. Must be an EOA, not a contract address + */ + function bondOperator(address _stakingProvider, address _operator) + external onlyOwnerOrStakingProvider(_stakingProvider) updateReward(_stakingProvider) + { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + address previousOperator = info.operator; + require(_operator != previousOperator, "Specified operator is already bonded with this provider"); + // If this staker had a operator ... + if (previousOperator != address(0)) { + require( + !info.operatorConfirmed || + block.timestamp >= uint256(info.operatorStartTimestamp) + minOperatorSeconds, + "Not enough time passed to change operator" + ); + // Remove the old relation "operator->stakingProvider" + _stakingProviderFromOperator[previousOperator] = address(0); + } + + if (_operator != address(0)) { + require(_stakingProviderFromOperator[_operator] == address(0), "Specified operator is already in use"); + require( + _operator == _stakingProvider || getBeneficiary(_operator) == address(0), + "Specified operator is a provider" + ); + // Set new operator->stakingProvider relation + _stakingProviderFromOperator[_operator] = _stakingProvider; + } + + if (info.operatorStartTimestamp == 0) { + stakingProviders.push(_stakingProvider); + } + + if (info.operatorConfirmed) { + authorizedOverall -= info.authorized; + } + + // Bond new operator (or unbond if _operator == address(0)) + info.operator = _operator; + info.operatorStartTimestamp = uint64(block.timestamp); + emit OperatorBonded(_stakingProvider, _operator, previousOperator, block.timestamp); + _releaseOperator(_stakingProvider); + } + + /** + * @notice Make a confirmation by operator + */ + function confirmOperatorAddress() external { + address stakingProvider = _stakingProviderFromOperator[msg.sender]; + require(isAuthorized(stakingProvider), "No stake associated with the operator"); + StakingProviderInfo storage info = stakingProviderInfo[stakingProvider]; + require(!info.operatorConfirmed, "Operator address is already confirmed"); + require(msg.sender == tx.origin, "Only operator with real address can make a confirmation"); + + updateRewardInternal(stakingProvider); + info.operatorConfirmed = true; + authorizedOverall += info.authorized; + emit OperatorConfirmed(stakingProvider, msg.sender); + + if (address(updatableStakeInfo) != address(0)) { + updatableStakeInfo.updateOperator(stakingProvider, msg.sender); + } + } + + //-------------------------XChain------------------------- + + /** + * @notice Resets operator confirmation + */ + function _releaseOperator(address _stakingProvider) internal { + stakingProviderInfo[_stakingProvider].operatorConfirmed = false; + if (address(updatableStakeInfo) != address(0)) { + updatableStakeInfo.updateOperator(_stakingProvider, address(0)); + } + } + + /** + * @notice Send updated authorized amount to xchain contract + */ + function _updateAuthorization(address _stakingProvider, StakingProviderInfo storage _info) internal { + if (address(updatableStakeInfo) != address(0)) { + // TODO send both authorized and eligible amounts in case of slashing from StakeInfo + uint96 eligibleAmount = getEligibleAmount(_info); + updatableStakeInfo.updateAmount(_stakingProvider, eligibleAmount); + } + } + + //-------------------------Slashing------------------------- + /** + * @notice Slash the provider's stake and reward the investigator + * @param _stakingProvider Staking provider address + * @param _penalty Penalty + * @param _investigator Investigator + */ + function slash( + address _stakingProvider, + uint96 _penalty, + address _investigator + ) + external + { + require(msg.sender == adjudicator, "Only adjudicator allowed to slash"); + address[] memory stakingProviderWrapper = new address[](1); + stakingProviderWrapper[0] = _stakingProvider; + tStaking.seize(_penalty, 100, _investigator, stakingProviderWrapper); + } + +} diff --git a/contracts/contracts/TestnetThresholdStaking.sol b/contracts/contracts/TestnetThresholdStaking.sol index dd3c60c1..22ac6c5f 100644 --- a/contracts/contracts/TestnetThresholdStaking.sol +++ b/contracts/contracts/TestnetThresholdStaking.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; -import "./SimplePREApplication.sol"; +import "@threshold/contracts/staking/IApplication.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; @@ -18,12 +18,12 @@ contract TestnetThresholdStaking is Ownable { uint96 nuInTStake; } - SimplePREApplication public preApplication; + IApplication public application; mapping (address => StakingProviderInfo) public stakingProviderInfo; - function setApplication(SimplePREApplication _preApplication) external onlyOwner { - preApplication = _preApplication; + function setApplication(IApplication _application) external onlyOwner { + application = _application; } function stakedNu(address) external view returns (uint256) { diff --git a/contracts/contracts/coordination/IUpdatableStakeInfo.sol b/contracts/contracts/coordination/IUpdatableStakeInfo.sol index a3130d85..8410b08e 100644 --- a/contracts/contracts/coordination/IUpdatableStakeInfo.sol +++ b/contracts/contracts/coordination/IUpdatableStakeInfo.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.0; /** -* @title StakeInfo -* @notice StakeInfo +* @title IUpdatableStakeInfo +* @notice Interface for x-chain interactions between application and coordinator */ -interface IUpdatableStakes { +interface IUpdatableStakeInfo { event UpdatedStakeOperator(address indexed stakingProvider, address indexed operator); event UpdatedStakeAmount(address indexed stakingProvider, uint96 amount); diff --git a/contracts/contracts/coordination/StakeInfo.sol b/contracts/contracts/coordination/StakeInfo.sol index 5e764313..710e5b3c 100644 --- a/contracts/contracts/coordination/StakeInfo.sol +++ b/contracts/contracts/coordination/StakeInfo.sol @@ -10,7 +10,7 @@ import "../../threshold/IAccessControlApplication.sol"; * @title StakeInfo * @notice StakeInfo */ -contract StakeInfo is AccessControl, IUpdatableStakes, IAccessControlApplication { +contract StakeInfo is AccessControl, IUpdatableStakeInfo, IAccessControlApplication { bytes32 public constant UPDATE_ROLE = keccak256("UPDATE_ROLE"); @@ -27,7 +27,7 @@ contract StakeInfo is AccessControl, IUpdatableStakes, IAccessControlApplication } mapping(address => Stake) public stakes; - mapping(address => address) public operatorToProvider; + mapping(address => address) private operatorToProvider; function stakingProviderFromOperator(address _operator) external view returns (address){ return operatorToProvider[_operator]; @@ -37,11 +37,11 @@ contract StakeInfo is AccessControl, IUpdatableStakes, IAccessControlApplication return stakes[_stakingProvider].amount; } - function updateOperator(address stakingProvider, address operator) external onlyRole(UPDATE_ROLE) { + function updateOperator(address stakingProvider, address operator) external override onlyRole(UPDATE_ROLE) { _updateOperator(stakingProvider, operator); } - function updateAmount(address stakingProvider, uint96 amount) external onlyRole(UPDATE_ROLE) { + function updateAmount(address stakingProvider, uint96 amount) external override onlyRole(UPDATE_ROLE) { _updateAmount(stakingProvider, amount); } @@ -69,7 +69,7 @@ contract StakeInfo is AccessControl, IUpdatableStakes, IAccessControlApplication } } - function batchUpdate(bytes32[] calldata updateInfo) external onlyRole(UPDATE_ROLE) { + function batchUpdate(bytes32[] calldata updateInfo) external override onlyRole(UPDATE_ROLE) { require(updateInfo.length % 2 == 0, "bad length"); for(uint i = 0; i < updateInfo.length; i += 2){ bytes32 word0 = updateInfo[i]; diff --git a/contracts/test/AdjudicatorTestSet.sol b/contracts/test/AdjudicatorTestSet.sol new file mode 100644 index 00000000..5e825455 --- /dev/null +++ b/contracts/test/AdjudicatorTestSet.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.0; + + +import "../contracts/Adjudicator.sol"; +import "../contracts/lib/SignatureVerifier.sol"; +//import "../contracts/proxy/Upgradeable.sol"; + + +/** +* @notice Contract for testing the Adjudicator contract +*/ +contract TACoApplicationForAdjudicatorMock { + + uint32 public immutable secondsPerPeriod = 1; + mapping (address => uint96) public stakingProviderInfo; + mapping (address => uint256) public rewardInfo; + mapping (address => address) _stakingProviderFromOperator; + + function stakingProviderFromOperator(address _operator) public view returns (address) { + return _stakingProviderFromOperator[_operator]; + } + + function setStakingProviderInfo(address _stakingProvider, uint96 _amount, address _operator) public { + stakingProviderInfo[_stakingProvider] = _amount; + if (_operator == address(0)) { + _operator = _stakingProvider; + } + _stakingProviderFromOperator[_operator] = _stakingProvider; + } + + function authorizedStake(address _stakingProvider) public view returns (uint96) { + return stakingProviderInfo[_stakingProvider]; + } + + function slash( + address _stakingProvider, + uint96 _penalty, + address _investigator + ) + external + { + stakingProviderInfo[_stakingProvider] -= _penalty; + rewardInfo[_investigator] += 1; + } + +} + + +///** +//* @notice Upgrade to this contract must lead to fail +//*/ +//contract AdjudicatorBad is Upgradeable { +// +// mapping (bytes32 => bool) public evaluatedCFrags; +// mapping (address => uint256) public penaltyHistory; +// +//} +// +// +///** +//* @notice Contract for testing upgrading the Adjudicator contract +//*/ +//contract AdjudicatorV2Mock is Adjudicator { +// +// uint256 public valueToCheck; +// +// constructor( +// SignatureVerifier.HashAlgorithm _hashAlgorithm, +// uint256 _basePenalty, +// uint256 _percentagePenalty, +// uint256 _penaltyHistoryCoefficient +// ) +// Adjudicator( +// _hashAlgorithm, +// _basePenalty, +// _percentagePenalty, +// _penaltyHistoryCoefficient +// ) +// { +// } +// +// function setValueToCheck(uint256 _valueToCheck) public { +// valueToCheck = _valueToCheck; +// } +// +// function verifyState(address _testTarget) override public { +// super.verifyState(_testTarget); +// require(uint256(delegateGet(_testTarget, this.valueToCheck.selector)) == valueToCheck); +// } +//} diff --git a/contracts/test/PREApplicationTestSet.sol b/contracts/test/PREApplicationTestSet.sol deleted file mode 100644 index 3ebd1b02..00000000 --- a/contracts/test/PREApplicationTestSet.sol +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -pragma solidity ^0.8.0; - - -import "../contracts/SimplePREApplication.sol"; -//import "zeppelin/token/ERC20/ERC20.sol"; -//import "zeppelin/token/ERC20/ERC20Detailed.sol"; - - -///** -//* @notice Contract for testing PRE application contract -//*/ -//contract TToken is ERC20, ERC20Detailed('T', 'T', 18) { -// -// constructor (uint256 _totalSupplyOfTokens) { -// _mint(msg.sender, _totalSupplyOfTokens); -// } -// -//} - - -/** -* @notice Contract for testing PRE application contract -*/ -contract ThresholdStakingForPREApplicationMock { - - struct StakingProviderInfo { - address owner; - address payable beneficiary; - address authorizer; - uint96 tStake; - uint96 keepInTStake; - uint96 nuInTStake; - } - - SimplePREApplication public preApplication; - - mapping (address => StakingProviderInfo) public stakingProviderInfo; - - function setApplication(SimplePREApplication _preApplication) external { - preApplication = _preApplication; - } - - function stakedNu(address) external view returns (uint256) { - return 0; - } - - function setRoles( - address _stakingProvider, - address _owner, - address payable _beneficiary, - address _authorizer - ) - external - { - StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; - info.owner = _owner; - info.beneficiary = _beneficiary; - info.authorizer = _authorizer; - } - - /** - * @dev If the function is called with only the _stakingProvider parameter, - * we presume that the caller wants that address set for the other roles as well. - */ - function setRoles(address _stakingProvider) external { - StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; - info.owner = _stakingProvider; - info.beneficiary = payable(_stakingProvider); - info.authorizer = _stakingProvider; - } - - function setStakes( - address _stakingProvider, - uint96 _tStake, - uint96 _keepInTStake, - uint96 _nuInTStake - ) - external - { - StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; - info.tStake = _tStake; - info.keepInTStake = _keepInTStake; - info.nuInTStake = _nuInTStake; - } - - function authorizedStake(address _stakingProvider, address _application) external view returns (uint96) { - return 0; - } - - function stakes(address _stakingProvider) external view returns ( - uint96 tStake, - uint96 keepInTStake, - uint96 nuInTStake - ) { - StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; - tStake = info.tStake; - keepInTStake = info.keepInTStake; - nuInTStake = info.nuInTStake; - } - - function rolesOf(address _stakingProvider) external view returns ( - address owner, - address payable beneficiary, - address authorizer - ) { - StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; - owner = info.owner; - beneficiary = info.beneficiary; - authorizer = info.authorizer; - } - -// function approveAuthorizationDecrease(address _stakingProvider) external returns (uint96) { -// -// } - -// function seize( -// uint96 _amount, -// uint256 _rewardMultipier, -// address _notifier, -// address[] memory _stakingProviders -// ) external { -// -// } - -// function authorizationIncreased(address _stakingProvider, uint96 _fromAmount, uint96 _toAmount) external { -// preApplication.authorizationIncreased(_stakingProvider, _fromAmount, _toAmount); -// } - -} - - -/** -* @notice Intermediary contract for testing operator -*/ -contract Intermediary { - - SimplePREApplication immutable preApplication; - - constructor(SimplePREApplication _preApplication) { - preApplication = _preApplication; - } - - function bondOperator(address _operator) external { - preApplication.bondOperator(address(this), _operator); - } - - function confirmOperatorAddress() external { - preApplication.confirmOperatorAddress(); - } - -} diff --git a/contracts/test/StakingEscrowTestSet.sol b/contracts/test/StakingEscrowTestSet.sol index 09424a72..14ee50c2 100644 --- a/contracts/test/StakingEscrowTestSet.sol +++ b/contracts/test/StakingEscrowTestSet.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../contracts/StakingEscrow.sol"; import "../contracts/NuCypherToken.sol"; -import "../threshold/IStaking.sol"; +import "@threshold/contracts/staking/IStaking.sol"; /** * @notice Enhanced version of StakingEscrow to use in tests diff --git a/contracts/test/TACoApplicationTestSet.sol b/contracts/test/TACoApplicationTestSet.sol new file mode 100644 index 00000000..3fbc6ac0 --- /dev/null +++ b/contracts/test/TACoApplicationTestSet.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.0; + + +import "../contracts/TACoApplication.sol"; +import "@threshold/contracts/staking/IApplication.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + + +/** +* @notice Contract for testing the application contract +*/ +contract TToken is ERC20("T", "T") { + + constructor (uint256 _totalSupplyOfTokens) { + _mint(msg.sender, _totalSupplyOfTokens); + } + +} + + +/** +* @notice Contract for testing TACo application contract +*/ +contract ThresholdStakingForTACoApplicationMock { + + struct StakingProviderInfo { + address owner; + address payable beneficiary; + address authorizer; + uint96 authorized; + uint96 decreaseRequestTo; + } + + IApplication public application; + + mapping (address => StakingProviderInfo) public stakingProviderInfo; + + uint96 public amountToSeize; + uint256 public rewardMultiplier; + address public notifier; + address[] public stakingProvidersToSeize; + + function setApplication(IApplication _application) external { + application = _application; + } + + function stakedNu(address) external view returns (uint256) { + return 0; + } + + function setRoles( + address _stakingProvider, + address _owner, + address payable _beneficiary, + address _authorizer + ) + public + { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + info.owner = _owner; + info.beneficiary = _beneficiary; + info.authorizer = _authorizer; + } + + /** + * @dev If the function is called with only the _stakingProvider parameter, + * we presume that the caller wants that address set for the other roles as well. + */ + function setRoles(address _stakingProvider) external { + setRoles(_stakingProvider, _stakingProvider, payable(_stakingProvider), _stakingProvider); + } + + function setAuthorized(address _stakingProvider, uint96 _authorized) external { + stakingProviderInfo[_stakingProvider].authorized = _authorized; + } + + function setDecreaseRequest(address _stakingProvider, uint96 _decreaseRequestTo) external { + stakingProviderInfo[_stakingProvider].decreaseRequestTo = _decreaseRequestTo; + } + + function authorizedStake(address _stakingProvider, address _application) external view returns (uint96) { + require(_stakingProvider == _application || _application == address(application)); + return stakingProviderInfo[_stakingProvider].authorized; + } + + function rolesOf(address _stakingProvider) external view returns ( + address owner, + address payable beneficiary, + address authorizer + ) { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + owner = info.owner; + beneficiary = info.beneficiary; + authorizer = info.authorizer; + } + + function approveAuthorizationDecrease(address _stakingProvider) external returns (uint96) { + StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; + info.authorized = info.decreaseRequestTo; + return info.authorized; + } + + function seize( + uint96 _amount, + uint256 _rewardMultiplier, + address _notifier, + address[] memory _stakingProviders + ) external { + amountToSeize = _amount; + rewardMultiplier = _rewardMultiplier; + notifier = _notifier; + stakingProvidersToSeize = _stakingProviders; + } + + function getLengthOfStakingProvidersToSeize() external view returns (uint256) { + return stakingProvidersToSeize.length; + } + + function authorizationIncreased(address _stakingProvider, uint96 _fromAmount, uint96 _toAmount) external { + application.authorizationIncreased(_stakingProvider, _fromAmount, _toAmount); + stakingProviderInfo[_stakingProvider].authorized = _toAmount; + } + + function involuntaryAuthorizationDecrease( + address _stakingProvider, + uint96 _fromAmount, + uint96 _toAmount + ) + external + { + application.involuntaryAuthorizationDecrease(_stakingProvider, _fromAmount, _toAmount); + stakingProviderInfo[_stakingProvider].authorized = _toAmount; + } + + function authorizationDecreaseRequested( + address _stakingProvider, + uint96 _fromAmount, + uint96 _toAmount + ) + external + { + application.authorizationDecreaseRequested(_stakingProvider, _fromAmount, _toAmount); + stakingProviderInfo[_stakingProvider].decreaseRequestTo = _toAmount; + } + +} + + +/** +* @notice Intermediary contract for testing operator +*/ +contract Intermediary { + + TACoApplication immutable public application; + + constructor(TACoApplication _application) { + application = _application; + } + + function bondOperator(address _operator) external { + application.bondOperator(address(this), _operator); + } + + function confirmOperatorAddress() external { + application.confirmOperatorAddress(); + } + +} diff --git a/contracts/threshold/IStaking.sol b/contracts/threshold/IStaking.sol deleted file mode 100644 index 06421d81..00000000 --- a/contracts/threshold/IStaking.sol +++ /dev/null @@ -1,379 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -// ██████████████ ▐████▌ ██████████████ -// ██████████████ ▐████▌ ██████████████ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ -// ██████████████ ▐████▌ ██████████████ -// ██████████████ ▐████▌ ██████████████ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ -// ▐████▌ ▐████▌ - -pragma solidity ^0.8.0; - -/// @title Interface of Threshold Network staking contract -/// @notice The staking contract enables T owners to have their wallets offline -/// and their stake managed by providers on their behalf. All off-chain -/// client software should be able to run without exposing provider’s -/// private key and should not require any owner’s keys at all. -/// The stake delegation optimizes the network throughput without -/// compromising the security of the owners’ stake. -interface IStaking { - enum StakeType { - NU, - KEEP, - T - } - - // - // - // Delegating a stake - // - // - - /// @notice Creates a delegation with `msg.sender` owner with the given - /// provider, beneficiary, and authorizer. Transfers the given - /// amount of T to the staking contract. - /// @dev The owner of the delegation needs to have the amount approved to - /// transfer to the staking contract. - function stake( - address stakingProvider, - address payable beneficiary, - address authorizer, - uint96 amount - ) external; - - /// @notice Copies delegation from the legacy KEEP staking contract to T - /// staking contract. No tokens are transferred. Caches the active - /// stake amount from KEEP staking contract. Can be called by - /// anyone. - function stakeKeep(address stakingProvider) external; - - /// @notice Copies delegation from the legacy NU staking contract to T - /// staking contract, additionally appointing beneficiary and - /// authorizer roles. Caches the amount staked in NU staking - /// contract. Can be called only by the original delegation owner. - function stakeNu( - address stakingProvider, - address payable beneficiary, - address authorizer - ) external; - - /// @notice Refresh Keep stake owner. Can be called only by the old owner. - function refreshKeepStakeOwner(address stakingProvider) external; - - /// @notice Allows the Governance to set the minimum required stake amount. - /// This amount is required to protect against griefing the staking - /// contract and individual applications are allowed to require - /// higher minimum stakes if necessary. - function setMinimumStakeAmount(uint96 amount) external; - - // - // - // Authorizing an application - // - // - - /// @notice Allows the Governance to approve the particular application - /// before individual stake authorizers are able to authorize it. - function approveApplication(address application) external; - - /// @notice Increases the authorization of the given provider for the given - /// application by the given amount. Can only be called by the given - /// provider’s authorizer. - /// @dev Calls `authorizationIncreased(address stakingProvider, uint256 amount)` - /// on the given application to notify the application about - /// authorization change. See `IApplication`. - function increaseAuthorization( - address stakingProvider, - address application, - uint96 amount - ) external; - - /// @notice Requests decrease of the authorization for the given provider on - /// the given application by the provided amount. - /// It may not change the authorized amount immediatelly. When - /// it happens depends on the application. Can only be called by the - /// given provider’s authorizer. Overwrites pending authorization - /// decrease for the given provider and application. - /// @dev Calls `authorizationDecreaseRequested(address stakingProvider, uint256 amount)` - /// on the given application. See `IApplication`. - function requestAuthorizationDecrease( - address stakingProvider, - address application, - uint96 amount - ) external; - - /// @notice Requests decrease of all authorizations for the given provider on - /// the applications by all authorized amount. - /// It may not change the authorized amount immediatelly. When - /// it happens depends on the application. Can only be called by the - /// given provider’s authorizer. Overwrites pending authorization - /// decrease for the given provider and application. - /// @dev Calls `authorizationDecreaseRequested(address stakingProvider, uint256 amount)` - /// for each authorized application. See `IApplication`. - function requestAuthorizationDecrease(address stakingProvider) external; - - /// @notice Called by the application at its discretion to approve the - /// previously requested authorization decrease request. Can only be - /// called by the application that was previously requested to - /// decrease the authorization for that provider. - /// Returns resulting authorized amount for the application. - function approveAuthorizationDecrease(address stakingProvider) - external - returns (uint96); - - /// @notice Decreases the authorization for the given `stakingProvider` on - /// the given disabled `application`, for all authorized amount. - /// Can be called by anyone. - function forceDecreaseAuthorization( - address stakingProvider, - address application - ) external; - - /// @notice Pauses the given application’s eligibility to slash stakes. - /// Besides that stakers can't change authorization to the application. - /// Can be called only by the Panic Button of the particular - /// application. The paused application can not slash stakes until - /// it is approved again by the Governance using `approveApplication` - /// function. Should be used only in case of an emergency. - function pauseApplication(address application) external; - - /// @notice Disables the given application. The disabled application can't - /// slash stakers. Also stakers can't increase authorization to that - /// application but can decrease without waiting by calling - /// `requestAuthorizationDecrease` at any moment. Can be called only - /// by the governance. The disabled application can't be approved - /// again. Should be used only in case of an emergency. - function disableApplication(address application) external; - - /// @notice Sets the Panic Button role for the given application to the - /// provided address. Can only be called by the Governance. If the - /// Panic Button for the given application should be disabled, the - /// role address should be set to 0x0 address. - function setPanicButton(address application, address panicButton) external; - - /// @notice Sets the maximum number of applications one provider can - /// authorize. Used to protect against DoSing slashing queue. - /// Can only be called by the Governance. - function setAuthorizationCeiling(uint256 ceiling) external; - - // - // - // Stake top-up - // - // - - /// @notice Increases the amount of the stake for the given provider. - /// Can be called only by the owner or provider. - /// @dev The sender of this transaction needs to have the amount approved to - /// transfer to the staking contract. - function topUp(address stakingProvider, uint96 amount) external; - - /// @notice Propagates information about stake top-up from the legacy KEEP - /// staking contract to T staking contract. Can be called only by - /// the owner or provider. - function topUpKeep(address stakingProvider) external; - - /// @notice Propagates information about stake top-up from the legacy NU - /// staking contract to T staking contract. Can be called only by - /// the owner or provider. - function topUpNu(address stakingProvider) external; - - // - // - // Undelegating a stake (unstaking) - // - // - - /// @notice Reduces the liquid T stake amount by the provided amount and - /// withdraws T to the owner. Reverts if there is at least one - /// authorization higher than the sum of the legacy stake and - /// remaining liquid T stake or if the unstake amount is higher than - /// the liquid T stake amount. Can be called only by the owner or - /// provider. - function unstakeT(address stakingProvider, uint96 amount) external; - - /// @notice Sets the legacy KEEP staking contract active stake amount cached - /// in T staking contract to 0. Reverts if the amount of liquid T - /// staked in T staking contract is lower than the highest - /// application authorization. This function allows to unstake from - /// KEEP staking contract and still being able to operate in T - /// network and earning rewards based on the liquid T staked. Can be - /// called only by the delegation owner and provider. - function unstakeKeep(address stakingProvider) external; - - /// @notice Reduces cached legacy NU stake amount by the provided amount. - /// Reverts if there is at least one authorization higher than the - /// sum of remaining legacy NU stake and liquid T stake for that - /// provider or if the untaked amount is higher than the cached - /// legacy stake amount. If succeeded, the legacy NU stake can be - /// partially or fully undelegated on the legacy staking contract. - /// This function allows to unstake from NU staking contract and - /// still being able to operate in T network and earning rewards - /// based on the liquid T staked. Can be called only by the - /// delegation owner and provider. - function unstakeNu(address stakingProvider, uint96 amount) external; - - /// @notice Sets cached legacy stake amount to 0, sets the liquid T stake - /// amount to 0 and withdraws all liquid T from the stake to the - /// owner. Reverts if there is at least one non-zero authorization. - /// Can be called only by the delegation owner and provider. - function unstakeAll(address stakingProvider) external; - - // - // - // Keeping information in sync - // - // - - /// @notice Notifies about the discrepancy between legacy KEEP active stake - /// and the amount cached in T staking contract. Slashes the provider - /// in case the amount cached is higher than the actual active stake - /// amount in KEEP staking contract. Needs to update authorizations - /// of all affected applications and execute an involuntary - /// allocation decrease on all affected applications. Can be called - /// by anyone, notifier receives a reward. - function notifyKeepStakeDiscrepancy(address stakingProvider) external; - - /// @notice Notifies about the discrepancy between legacy NU active stake - /// and the amount cached in T staking contract. Slashes the - /// provider in case the amount cached is higher than the actual - /// active stake amount in NU staking contract. Needs to update - /// authorizations of all affected applications and execute an - /// involuntary allocation decrease on all affected applications. - /// Can be called by anyone, notifier receives a reward. - function notifyNuStakeDiscrepancy(address stakingProvider) external; - - /// @notice Sets the penalty amount for stake discrepancy and reward - /// multiplier for reporting it. The penalty is seized from the - /// provider account, and 5% of the penalty, scaled by the - /// multiplier, is given to the notifier. The rest of the tokens are - /// burned. Can only be called by the Governance. See `seize` function. - function setStakeDiscrepancyPenalty( - uint96 penalty, - uint256 rewardMultiplier - ) external; - - /// @notice Sets reward in T tokens for notification of misbehaviour - /// of one provider. Can only be called by the governance. - function setNotificationReward(uint96 reward) external; - - /// @notice Transfer some amount of T tokens as reward for notifications - /// of misbehaviour - function pushNotificationReward(uint96 reward) external; - - /// @notice Withdraw some amount of T tokens from notifiers treasury. - /// Can only be called by the governance. - function withdrawNotificationReward(address recipient, uint96 amount) - external; - - /// @notice Adds providers to the slashing queue along with the amount that - /// should be slashed from each one of them. Can only be called by - /// application authorized for all providers in the array. - function slash(uint96 amount, address[] memory stakingProviders) external; - - /// @notice Adds providers to the slashing queue along with the amount. - /// The notifier will receive reward per each provider from - /// notifiers treasury. Can only be called by application - /// authorized for all providers in the array. - function seize( - uint96 amount, - uint256 rewardMultipier, - address notifier, - address[] memory stakingProviders - ) external; - - /// @notice Takes the given number of queued slashing operations and - /// processes them. Receives 5% of the slashed amount. - /// Executes `involuntaryAllocationDecrease` function on each - /// affected application. - function processSlashing(uint256 count) external; - - // - // - // Auxiliary functions - // - // - - /// @notice Returns the authorized stake amount of the provider for the - /// application. - function authorizedStake(address stakingProvider, address application) - external - view - returns (uint96); - - /// @notice Returns staked amount of T, Keep and Nu for the specified - /// staking provider. - /// @dev All values are in T denomination - function stakes(address stakingProvider) - external - view - returns ( - uint96 tStake, - uint96 keepInTStake, - uint96 nuInTStake - ); - - /// @notice Returns start staking timestamp for T/NU stake. - /// @dev This value is set at most once, and only when a stake is created - /// with T or NU tokens. If a stake is created from a legacy KEEP - /// stake, this value will remain as zero - function getStartStakingTimestamp(address stakingProvider) - external - view - returns (uint256); - - /// @notice Returns staked amount of NU for the specified provider - function stakedNu(address stakingProvider) external view returns (uint256); - - /// @notice Gets the stake owner, the beneficiary and the authorizer - /// for the specified provider address. - /// @return owner Stake owner address. - /// @return beneficiary Beneficiary address. - /// @return authorizer Authorizer address. - function rolesOf(address stakingProvider) - external - view - returns ( - address owner, - address payable beneficiary, - address authorizer - ); - - /// @notice Returns length of application array - function getApplicationsLength() external view returns (uint256); - - /// @notice Returns length of slashing queue - function getSlashingQueueLength() external view returns (uint256); - - /// @notice Returns minimum possible stake for T, KEEP or NU in T denomination - /// @dev For example, suppose the given provider has 10 T, 20 T worth - /// of KEEP, and 30 T worth of NU all staked, and the maximum - /// application authorization is 40 T, then `getMinStaked` for - /// that provider returns: - /// * 0 T if KEEP stake type specified i.e. - /// min = 40 T max - (10 T + 30 T worth of NU) = 0 T - /// * 10 T if NU stake type specified i.e. - /// min = 40 T max - (10 T + 20 T worth of KEEP) = 10 T - /// * 0 T if T stake type specified i.e. - /// min = 40 T max - (20 T worth of KEEP + 30 T worth of NU) < 0 T - /// In other words, the minimum stake amount for the specified - /// stake type is the minimum amount of stake of the given type - /// needed to satisfy the maximum application authorization given - /// the staked amounts of the other stake types for that provider. - function getMinStaked(address stakingProvider, StakeType stakeTypes) - external - view - returns (uint96); - - /// @notice Returns available amount to authorize for the specified application - function getAvailableToAuthorize( - address stakingProvider, - address application - ) external view returns (uint96); -} diff --git a/contracts/xchain/PolygonRoot.sol b/contracts/xchain/PolygonRoot.sol index 50ce0ba7..0bd6f1a6 100644 --- a/contracts/xchain/PolygonRoot.sol +++ b/contracts/xchain/PolygonRoot.sol @@ -2,29 +2,50 @@ pragma solidity ^0.8.0; import "@fx-portal/contracts/tunnel/FxBaseRootTunnel.sol"; +import "../contracts/coordination/IUpdatableStakeInfo.sol"; -contract PolygonRoot is FxBaseRootTunnel { +contract PolygonRoot is FxBaseRootTunnel, IUpdatableStakeInfo { + + address immutable public source; bytes public latestData; - constructor(address _checkpointManager, address _fxRoot) FxBaseRootTunnel(_checkpointManager, _fxRoot) {} + constructor( + address _checkpointManager, + address _fxRoot, + address _source + ) + FxBaseRootTunnel(_checkpointManager, _fxRoot) + { + require(_source != address(0), "Wrong input parameters"); + source = _source; + } + + /** + * @dev Checks caller is source of data + */ + modifier onlySource() + { + require(msg.sender == source, "Caller must be the source"); + _; + } function _processMessageFromChild(bytes memory data) internal override { latestData = data; } - function updateOperator(address stakingProvider, address operator) public { - bytes memory message = abi.encodeWithSignature("updateOperator(address,address)", stakingProvider, operator); + function updateOperator(address stakingProvider, address operator) external override onlySource { + bytes memory message = abi.encodeWithSelector(IUpdatableStakeInfo.updateOperator.selector, stakingProvider, operator); _sendMessageToChild(message); } - function updateAmount(address stakingProvider, uint96 amount) public { - bytes memory message = abi.encodeWithSignature("updateAmount(address,uint96)", stakingProvider, amount); + function updateAmount(address stakingProvider, uint96 amount) external override onlySource { + bytes memory message = abi.encodeWithSelector(IUpdatableStakeInfo.updateAmount.selector, stakingProvider, amount); _sendMessageToChild(message); } - function batchUpdate(bytes32[] calldata updateInfo) public { - bytes memory message = abi.encodeWithSignature("batchUpdate(bytes32[])", updateInfo); + function batchUpdate(bytes32[] calldata updateInfo) external override onlySource { + bytes memory message = abi.encodeWithSelector(IUpdatableStakeInfo.batchUpdate.selector, updateInfo); _sendMessageToChild(message); } } \ No newline at end of file diff --git a/scripts/deploy_staking_escrow.py b/scripts/deploy_staking_escrow.py index 6a18e77d..ef9f3841 100644 --- a/scripts/deploy_staking_escrow.py +++ b/scripts/deploy_staking_escrow.py @@ -13,7 +13,7 @@ def main(account_id=None): deployer = get_account(account_id) deployments_config = DEPLOYMENTS_CONFIG if CURRENT_NETWORK in LOCAL_BLOCKCHAIN_ENVIRONMENTS: - nucypher_token, t_staking, _, work_lock, _ = deploy_mocks(deployer) + nucypher_token, t_staking, _, work_lock, _, _ = deploy_mocks(deployer) else: nucypher_token = deployments_config.get("nu_token") t_staking = deployments_config.get("t_staking") diff --git a/scripts/deploy_simple_pre.py b/scripts/deploy_taco_application.py similarity index 65% rename from scripts/deploy_simple_pre.py rename to scripts/deploy_taco_application.py index f1d32820..b21d911d 100644 --- a/scripts/deploy_simple_pre.py +++ b/scripts/deploy_taco_application.py @@ -14,15 +14,20 @@ def main(account_id=None): deployments_config = DEPLOYMENTS_CONFIG if CURRENT_NETWORK in LOCAL_BLOCKCHAIN_ENVIRONMENTS: - _, _, t_staking, _, _ = deploy_mocks(deployer) + _, _, t_staking, _, _, t_token = deploy_mocks(deployer) else: t_staking = deployments_config.get("t_staking") + t_token = deployments_config.get("t_token") - simple_pre = project.SimplePREApplication.deploy( + # TODO deploy proxy + taco_app = project.TACoApplication.deploy( + t_token, t_staking, deployments_config.get("pre_min_authorization"), deployments_config.get("pre_min_operator_seconds"), + deployments_config.get("reward_duration"), + deployments_config.get("deauthorization_duration"), sender=deployer, publish=deployments_config.get("verify"), ) - return simple_pre + return taco_app diff --git a/scripts/utils.py b/scripts/utils.py index 37c96784..ff54f1e5 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -1,4 +1,5 @@ from ape import accounts, config, networks, project +from web3 import Web3 LOCAL_BLOCKCHAIN_ENVIRONMENTS = ["local"] PRODUCTION_ENVIRONMENTS = ["mainnet", "polygon-main"] @@ -11,12 +12,21 @@ def deploy_mocks(deployer): corresponding contract addresses""" nucypher_token = project.NuCypherToken.deploy(1_000_000_000, sender=deployer) t_staking_for_escrow = project.ThresholdStakingForStakingEscrowMock.deploy(sender=deployer) - t_staking_for_pre = project.ThresholdStakingForPREApplicationMock.deploy(sender=deployer) + t_staking_for_taco = project.ThresholdStakingForTACoApplicationMock.deploy(sender=deployer) + total_supply = Web3.to_wei(10_000_000_000, "ether") + t_token = project.TToken.deploy(total_supply, sender=deployer) work_lock = project.WorkLockForStakingEscrowMock.deploy(nucypher_token, sender=deployer) staking_escrow = project.StakingEscrow.deploy( nucypher_token, work_lock, t_staking_for_escrow, sender=deployer ) - return nucypher_token, t_staking_for_escrow, t_staking_for_pre, work_lock, staking_escrow + return ( + nucypher_token, + t_staking_for_escrow, + t_staking_for_taco, + work_lock, + staking_escrow, + t_token, + ) def get_account(id): diff --git a/tests/application/conftest.py b/tests/application/conftest.py index bdc6b003..59810084 100644 --- a/tests/application/conftest.py +++ b/tests/application/conftest.py @@ -20,18 +20,11 @@ from ape import project from web3 import Web3 - MIN_AUTHORIZATION = Web3.to_wei(40_000, "ether") MIN_OPERATOR_SECONDS = 24 * 60 * 60 +TOTAL_SUPPLY = Web3.to_wei(10_000_000_000, "ether") -HASH_ALGORITHM_KECCAK256 = 0 -HASH_ALGORITHM_SHA256 = 1 -HASH_ALGORITHM_RIPEMD160 = 2 -HASH_ALGORITHM = HASH_ALGORITHM_SHA256 -BASE_PENALTY = 2 -PENALTY_HISTORY_COEFFICIENT = 0 -PERCENTAGE_PENALTY_COEFFICIENT = 100000 REWARD_DURATION = 60 * 60 * 24 * 7 # one week in seconds DEAUTHORIZATION_DURATION = 60 * 60 * 24 * 60 # 60 days in seconds TOTAL_SUPPLY = Web3.to_wei(1_000_000_000, "ether") # TODO NU(1_000_000_000, 'NU').to_units() @@ -48,16 +41,60 @@ def token(project, accounts): @pytest.fixture() def threshold_staking(project, accounts): - threshold_staking = accounts[0].deploy(project.ThresholdStakingForPREApplicationMock) + threshold_staking = accounts[0].deploy(project.ThresholdStakingForTACoApplicationMock) return threshold_staking +def encode_function_data(initializer=None, *args): + """Encodes the function call so we can work with an initializer. + Args: + initializer ([ape.Contract.ContractMethodHandler], optional): + The initializer function we want to call. Example: `box.store`. + Defaults to None. + args (Any, optional): + The arguments to pass to the initializer function + Returns: + [bytes]: Return the encoded bytes. + """ + if not len(args): + args = b"" + + if initializer: + return initializer.encode_input(*args) + + return b"" + + @pytest.fixture() -def pre_application(project, accounts, threshold_staking): - contract = accounts[0].deploy( - project.SimplePREApplication, threshold_staking.address, MIN_AUTHORIZATION, MIN_OPERATOR_SECONDS +def taco_application(project, creator, token, threshold_staking): + contract = creator.deploy( + project.TACoApplication, + token.address, + threshold_staking.address, + MIN_AUTHORIZATION, + MIN_OPERATOR_SECONDS, + REWARD_DURATION, + DEAUTHORIZATION_DURATION, ) - threshold_staking.setApplication(contract.address, sender=accounts[0]) + proxy_admin = DEPENDENCY.ProxyAdmin.deploy(sender=creator) + encoded_initializer_function = encode_function_data() + proxy = DEPENDENCY.TransparentUpgradeableProxy.deploy( + contract.address, + proxy_admin.address, + encoded_initializer_function, + sender=creator, + ) + proxy_contract = project.TACoApplication.at(proxy.address) + + threshold_staking.setApplication(proxy_contract.address, sender=creator) + proxy_contract.initialize(sender=creator) + + return proxy_contract + +@pytest.fixture() +def stake_info(project, creator, taco_application): + contract = project.StakeInfo.deploy([taco_application.address], sender=creator) + taco_application.setUpdatableStakeInfo(contract.address, sender=creator) return contract diff --git a/tests/application/test_authorization.py b/tests/application/test_authorization.py new file mode 100644 index 00000000..52422696 --- /dev/null +++ b/tests/application/test_authorization.py @@ -0,0 +1,667 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + +import ape +from ape.utils import ZERO_ADDRESS +from web3 import Web3 + +OPERATOR_CONFIRMED_SLOT = 1 +AUTHORIZATION_SLOT = 3 +DEAUTHORIZING_SLOT = 4 +END_DEAUTHORIZATION_SLOT = 5 +MIN_AUTHORIZATION = Web3.to_wei(40_000, "ether") +DEAUTHORIZATION_DURATION = 60 * 60 * 24 * 60 # 60 days in seconds + +STAKE_INFO_OPERATOR_SLOT = 0 + + +def test_authorization_increase(accounts, threshold_staking, taco_application, stake_info): + """ + Tests for authorization method: authorizationIncreased + """ + + creator, staking_provider = accounts[0:2] + minimum_authorization = MIN_AUTHORIZATION + value = minimum_authorization + + # Can't call `authorizationIncreased` directly + with ape.reverts(): + taco_application.authorizationIncreased(staking_provider, 0, value, sender=creator) + + # Staking provider and toAmount must be specified + with ape.reverts(): + threshold_staking.authorizationIncreased(ZERO_ADDRESS, 0, value, sender=creator) + + with ape.reverts(): + threshold_staking.authorizationIncreased(staking_provider, 0, 0, sender=creator) + + # Authorization must be greater than minimum + with ape.reverts(): + threshold_staking.authorizationIncreased(staking_provider, 0, value - 1, sender=creator) + + # First authorization + tx = threshold_staking.authorizationIncreased(staking_provider, 0, value, sender=creator) + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == value + assert taco_application.authorizedOverall() == 0 + assert taco_application.authorizedStake(staking_provider) == value + assert stake_info.authorizedStake(staking_provider) == value + assert taco_application.isAuthorized(staking_provider) + + # Check that all events are emitted + events = taco_application.AuthorizationIncreased.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == 0 + assert event["toAmount"] == value + + # Decrease and try to increase again + threshold_staking.involuntaryAuthorizationDecrease( + staking_provider, value, value // 2, sender=creator + ) + + # Resulting authorization must be greater than minimum + with ape.reverts(): + threshold_staking.authorizationIncreased( + staking_provider, value // 2, value - 1, sender=creator + ) + + tx = threshold_staking.authorizationIncreased( + staking_provider, value // 2, value, sender=creator + ) + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == value + assert taco_application.authorizedOverall() == 0 + assert taco_application.authorizedStake(staking_provider) == value + assert stake_info.authorizedStake(staking_provider) == value + assert taco_application.isAuthorized(staking_provider) + + events = taco_application.AuthorizationIncreased.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value // 2 + assert event["toAmount"] == value + + # Confirm operator address and try to increase authorization again + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + taco_application.confirmOperatorAddress(sender=staking_provider) + + authorization = 2 * value + 1 + tx = threshold_staking.authorizationIncreased( + staking_provider, value, authorization, sender=creator + ) + assert ( + taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == authorization + ) + assert taco_application.authorizedOverall() == authorization + assert taco_application.authorizedStake(staking_provider) == authorization + assert stake_info.authorizedStake(staking_provider) == authorization + assert taco_application.isAuthorized(staking_provider) + + events = taco_application.AuthorizationIncreased.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == authorization + + # Emulate slash and desync by sending smaller fromAmount + tx = threshold_staking.authorizationIncreased( + staking_provider, value // 2, value, sender=creator + ) + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == value + assert taco_application.authorizedOverall() == value + assert taco_application.authorizedStake(staking_provider) == value + assert stake_info.authorizedStake(staking_provider) == value + assert taco_application.isAuthorized(staking_provider) + + events = taco_application.AuthorizationIncreased.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value // 2 + assert event["toAmount"] == value + + # Increase again without syncing with StakeInfo + taco_application.setUpdatableStakeInfo(ZERO_ADDRESS, sender=creator) + tx = threshold_staking.authorizationIncreased( + staking_provider, value, 2 * value, sender=creator + ) + assert taco_application.authorizedStake(staking_provider) == 2 * value + assert stake_info.authorizedStake(staking_provider) == value + + +def test_involuntary_authorization_decrease( + accounts, threshold_staking, taco_application, stake_info +): + """ + Tests for authorization method: involuntaryAuthorizationDecrease + """ + + creator, staking_provider = accounts[0:2] + minimum_authorization = MIN_AUTHORIZATION + value = minimum_authorization + + # Prepare staking providers + threshold_staking.authorizationIncreased(staking_provider, 0, value, sender=creator) + + # Can't call `involuntaryAuthorizationDecrease` directly + with ape.reverts(): + taco_application.involuntaryAuthorizationDecrease( + staking_provider, value, 0, sender=creator + ) + + authorization = value // 2 + tx = threshold_staking.involuntaryAuthorizationDecrease( + staking_provider, value, value // 2, sender=creator + ) + assert ( + taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == authorization + ) + assert taco_application.authorizedOverall() == 0 + assert taco_application.authorizedStake(staking_provider) == authorization + assert stake_info.authorizedStake(staking_provider) == authorization + assert taco_application.isAuthorized(staking_provider) + assert not taco_application.isOperatorConfirmed(staking_provider) + assert not taco_application.stakingProviderInfo(staking_provider)[OPERATOR_CONFIRMED_SLOT] + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == ZERO_ADDRESS + + events = taco_application.AuthorizationInvoluntaryDecreased.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == authorization + + # Prepare request to decrease before involuntary decrease + threshold_staking.authorizationDecreaseRequested( + staking_provider, value // 2, 0, sender=creator + ) + authorization = value // 4 + tx = threshold_staking.involuntaryAuthorizationDecrease( + staking_provider, value // 2, authorization, sender=creator + ) + assert ( + taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == authorization + ) + assert ( + taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == authorization + ) + assert taco_application.authorizedOverall() == 0 + assert taco_application.authorizedStake(staking_provider) == authorization + assert stake_info.authorizedStake(staking_provider) == 0 + assert taco_application.isAuthorized(staking_provider) + assert not taco_application.isOperatorConfirmed(staking_provider) + assert not taco_application.stakingProviderInfo(staking_provider)[OPERATOR_CONFIRMED_SLOT] + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == ZERO_ADDRESS + + events = taco_application.AuthorizationInvoluntaryDecreased.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value // 2 + assert event["toAmount"] == authorization + + # Confirm operator address and decrease again + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + taco_application.confirmOperatorAddress(sender=staking_provider) + + authorization = value // 8 + tx = threshold_staking.involuntaryAuthorizationDecrease( + staking_provider, value // 4, authorization, sender=creator + ) + assert ( + taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == authorization + ) + assert ( + taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == authorization + ) + assert taco_application.authorizedOverall() == authorization + assert taco_application.authorizedStake(staking_provider) == authorization + assert stake_info.authorizedStake(staking_provider) == 0 + assert taco_application.isAuthorized(staking_provider) + assert taco_application.isOperatorConfirmed(staking_provider) + assert taco_application.getOperatorFromStakingProvider(staking_provider) == staking_provider + assert taco_application.stakingProviderFromOperator(staking_provider) == staking_provider + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == staking_provider + + events = taco_application.AuthorizationInvoluntaryDecreased.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value // 4 + assert event["toAmount"] == authorization + + # Decrease everything + tx = threshold_staking.involuntaryAuthorizationDecrease( + staking_provider, authorization, 0, sender=creator + ) + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == 0 + assert taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == 0 + assert taco_application.authorizedOverall() == 0 + assert taco_application.authorizedStake(staking_provider) == 0 + assert stake_info.authorizedStake(staking_provider) == 0 + assert not taco_application.isAuthorized(staking_provider) + assert not taco_application.isOperatorConfirmed(staking_provider) + assert not taco_application.stakingProviderInfo(staking_provider)[OPERATOR_CONFIRMED_SLOT] + assert taco_application.getOperatorFromStakingProvider(staking_provider) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(staking_provider) == ZERO_ADDRESS + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == ZERO_ADDRESS + + events = taco_application.AuthorizationInvoluntaryDecreased.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == authorization + assert event["toAmount"] == 0 + + # Emulate slash and desync by sending smaller fromAmount + threshold_staking.authorizationIncreased(staking_provider, 0, 2 * value, sender=creator) + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + taco_application.confirmOperatorAddress(sender=staking_provider) + + authorization = value // 2 + tx = threshold_staking.involuntaryAuthorizationDecrease( + staking_provider, value, value // 2, sender=creator + ) + assert ( + taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == authorization + ) + assert taco_application.authorizedOverall() == authorization + assert taco_application.authorizedStake(staking_provider) == authorization + assert stake_info.authorizedStake(staking_provider) == authorization + + events = taco_application.AuthorizationInvoluntaryDecreased.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == authorization + + # Decrease everything again without syncing with StakeInfo + taco_application.setUpdatableStakeInfo(ZERO_ADDRESS, sender=creator) + threshold_staking.involuntaryAuthorizationDecrease( + staking_provider, authorization, 0, sender=creator + ) + assert stake_info.authorizedStake(staking_provider) == authorization + assert taco_application.getOperatorFromStakingProvider(staking_provider) == ZERO_ADDRESS + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == staking_provider + + +def test_authorization_decrease_request( + accounts, threshold_staking, taco_application, stake_info, chain +): + """ + Tests for authorization method: authorizationDecreaseRequested + """ + + creator, staking_provider = accounts[0:2] + deauthorization_duration = DEAUTHORIZATION_DURATION + minimum_authorization = MIN_AUTHORIZATION + value = 2 * minimum_authorization + 1 + + # Prepare staking providers + threshold_staking.authorizationIncreased(staking_provider, 0, value, sender=creator) + + # Can't call `involuntaryAuthorizationDecrease` directly + with ape.reverts(): + taco_application.authorizationDecreaseRequested(staking_provider, value, 0, sender=creator) + + # Can't increase amount using request + with ape.reverts(): + threshold_staking.authorizationDecreaseRequested( + staking_provider, value, value + 1, sender=creator + ) + + # Resulting amount must be greater than minimum or 0 + with ape.reverts(): + threshold_staking.authorizationDecreaseRequested(staking_provider, value, 1, sender=creator) + + # Request of partial decrease + tx = threshold_staking.authorizationDecreaseRequested( + staking_provider, value, minimum_authorization, sender=creator + ) + timestamp = chain.pending_timestamp - 1 + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == value + assert ( + taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] + == minimum_authorization + 1 + ) + end_deauthorization = timestamp + deauthorization_duration + assert ( + taco_application.stakingProviderInfo(staking_provider)[END_DEAUTHORIZATION_SLOT] + == end_deauthorization + ) + assert taco_application.authorizedOverall() == 0 + assert taco_application.authorizedStake(staking_provider) == value + assert stake_info.authorizedStake(staking_provider) == minimum_authorization + assert taco_application.isAuthorized(staking_provider) + + events = taco_application.AuthorizationDecreaseRequested.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == minimum_authorization + + # Confirm operator address and request full decrease + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + taco_application.confirmOperatorAddress(sender=staking_provider) + + tx = threshold_staking.authorizationDecreaseRequested( + staking_provider, value, 0, sender=creator + ) + timestamp = chain.pending_timestamp - 1 + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == value + assert taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == value + end_deauthorization = timestamp + deauthorization_duration + assert ( + taco_application.stakingProviderInfo(staking_provider)[END_DEAUTHORIZATION_SLOT] + == end_deauthorization + ) + assert taco_application.authorizedOverall() == value + assert taco_application.authorizedStake(staking_provider) == value + assert stake_info.authorizedStake(staking_provider) == 0 + assert taco_application.isAuthorized(staking_provider) + + events = taco_application.AuthorizationDecreaseRequested.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == 0 + + # Emulate slash and desync by sending smaller fromAmount + tx = threshold_staking.authorizationDecreaseRequested( + staking_provider, value // 2, 0, sender=creator + ) + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == value // 2 + assert taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == value // 2 + assert taco_application.authorizedOverall() == value // 2 + assert taco_application.authorizedStake(staking_provider) == value // 2 + assert stake_info.authorizedStake(staking_provider) == 0 + + events = taco_application.AuthorizationDecreaseRequested.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value // 2 + assert event["toAmount"] == 0 + + # Request decrease without syncing with StakeInfo + taco_application.setUpdatableStakeInfo(ZERO_ADDRESS, sender=creator) + threshold_staking.authorizationDecreaseRequested( + staking_provider, value // 2, value // 2, sender=creator + ) + assert stake_info.authorizedStake(staking_provider) == 0 + + +def test_finish_authorization_decrease( + accounts, threshold_staking, taco_application, stake_info, chain +): + """ + Tests for authorization method: finishAuthorizationDecrease + """ + + creator, staking_provider = accounts[0:2] + deauthorization_duration = DEAUTHORIZATION_DURATION + minimum_authorization = MIN_AUTHORIZATION + value = 3 * minimum_authorization + + # Prepare staking providers + threshold_staking.authorizationIncreased(staking_provider, 0, value, sender=creator) + + # Can't approve decrease without request + with ape.reverts(): + taco_application.finishAuthorizationDecrease(staking_provider, sender=creator) + + new_value = 2 * minimum_authorization + threshold_staking.authorizationDecreaseRequested( + staking_provider, value, new_value, sender=creator + ) + + # Can't approve decrease before end timestamp + with ape.reverts(): + taco_application.finishAuthorizationDecrease(staking_provider, sender=creator) + + # Wait some time + chain.pending_timestamp += deauthorization_duration + tx = taco_application.finishAuthorizationDecrease(staking_provider, sender=creator) + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == new_value + assert taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == 0 + assert taco_application.stakingProviderInfo(staking_provider)[END_DEAUTHORIZATION_SLOT] == 0 + assert taco_application.authorizedOverall() == 0 + assert taco_application.authorizedStake(staking_provider) == new_value + assert stake_info.authorizedStake(staking_provider) == new_value + assert taco_application.isAuthorized(staking_provider) + assert ( + threshold_staking.authorizedStake(staking_provider, taco_application.address) == new_value + ) + + events = taco_application.AuthorizationDecreaseApproved.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == new_value + + # Confirm operator, request again then desync values and finish decrease + value = new_value + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + taco_application.confirmOperatorAddress(sender=staking_provider) + threshold_staking.authorizationDecreaseRequested( + staking_provider, value, minimum_authorization, sender=creator + ) + + new_value = minimum_authorization // 2 + threshold_staking.setDecreaseRequest(staking_provider, new_value, sender=creator) + chain.pending_timestamp += deauthorization_duration + tx = taco_application.finishAuthorizationDecrease(staking_provider, sender=creator) + + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == new_value + assert taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == 0 + assert taco_application.stakingProviderInfo(staking_provider)[END_DEAUTHORIZATION_SLOT] == 0 + assert taco_application.getOperatorFromStakingProvider(staking_provider) == staking_provider + assert taco_application.stakingProviderFromOperator(staking_provider) == staking_provider + assert taco_application.authorizedOverall() == new_value + assert taco_application.authorizedStake(staking_provider) == new_value + assert stake_info.authorizedStake(staking_provider) == new_value + assert taco_application.isAuthorized(staking_provider) + assert taco_application.isOperatorConfirmed(staking_provider) + assert ( + threshold_staking.authorizedStake(staking_provider, taco_application.address) == new_value + ) + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == staking_provider + + events = taco_application.AuthorizationDecreaseApproved.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == new_value + + # Decrease everything + value = new_value + threshold_staking.authorizationDecreaseRequested(staking_provider, value, 0, sender=creator) + chain.pending_timestamp += deauthorization_duration + tx = taco_application.finishAuthorizationDecrease(staking_provider, sender=creator) + + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == 0 + assert taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == 0 + assert taco_application.stakingProviderInfo(staking_provider)[END_DEAUTHORIZATION_SLOT] == 0 + assert taco_application.getOperatorFromStakingProvider(staking_provider) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(staking_provider) == ZERO_ADDRESS + assert taco_application.authorizedOverall() == 0 + assert taco_application.authorizedStake(staking_provider) == 0 + assert stake_info.authorizedStake(staking_provider) == 0 + assert not taco_application.isAuthorized(staking_provider) + assert not taco_application.isOperatorConfirmed(staking_provider) + assert not taco_application.stakingProviderInfo(staking_provider)[OPERATOR_CONFIRMED_SLOT] + assert threshold_staking.authorizedStake(staking_provider, taco_application.address) == 0 + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == ZERO_ADDRESS + + events = taco_application.AuthorizationDecreaseApproved.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == 0 + + # Decrease everything again without syncing with StakeInfo + value = minimum_authorization + threshold_staking.authorizationIncreased(staking_provider, 0, 2 * value, sender=creator) + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + taco_application.confirmOperatorAddress(sender=staking_provider) + threshold_staking.authorizationDecreaseRequested( + staking_provider, 2 * value, value, sender=creator + ) + chain.pending_timestamp += deauthorization_duration + taco_application.setUpdatableStakeInfo(ZERO_ADDRESS, sender=creator) + threshold_staking.setDecreaseRequest(staking_provider, 0, sender=creator) + taco_application.finishAuthorizationDecrease(staking_provider, sender=creator) + assert taco_application.authorizedStake(staking_provider) == 0 + assert stake_info.authorizedStake(staking_provider) == value + assert taco_application.getOperatorFromStakingProvider(staking_provider) == ZERO_ADDRESS + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == staking_provider + + +def test_resync(accounts, threshold_staking, taco_application, stake_info): + """ + Tests for authorization method: resynchronizeAuthorization + """ + + creator, staking_provider = accounts[0:2] + minimum_authorization = MIN_AUTHORIZATION + value = 3 * minimum_authorization + + # Nothing sync for not staking provider + with ape.reverts(): + taco_application.resynchronizeAuthorization(staking_provider, sender=creator) + + # Prepare staking providers + threshold_staking.authorizationIncreased(staking_provider, 0, value, sender=creator) + + # Nothing to resync + with ape.reverts(): + taco_application.resynchronizeAuthorization(staking_provider, sender=creator) + + # Change authorized amount and resync + new_value = 2 * minimum_authorization + threshold_staking.setAuthorized(staking_provider, new_value, sender=creator) + tx = taco_application.resynchronizeAuthorization(staking_provider, sender=creator) + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == new_value + assert taco_application.authorizedOverall() == 0 + assert taco_application.authorizedStake(staking_provider) == new_value + assert stake_info.authorizedStake(staking_provider) == new_value + assert taco_application.isAuthorized(staking_provider) + + events = taco_application.AuthorizationReSynchronized.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == new_value + + # Confirm operator and change authorized amount again + value = new_value + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + taco_application.confirmOperatorAddress(sender=staking_provider) + + new_value = minimum_authorization + threshold_staking.setAuthorized(staking_provider, new_value, sender=creator) + tx = taco_application.resynchronizeAuthorization(staking_provider, sender=creator) + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == new_value + assert taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == 0 + assert taco_application.authorizedOverall() == new_value + assert taco_application.getOperatorFromStakingProvider(staking_provider) == staking_provider + assert taco_application.stakingProviderFromOperator(staking_provider) == staking_provider + assert taco_application.authorizedOverall() == new_value + assert taco_application.authorizedStake(staking_provider) == new_value + assert stake_info.authorizedStake(staking_provider) == new_value + assert taco_application.isAuthorized(staking_provider) + assert taco_application.isOperatorConfirmed(staking_provider) + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == staking_provider + + events = taco_application.AuthorizationReSynchronized.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == new_value + + # Request decrease and change authorized amount again + value = new_value + threshold_staking.authorizationDecreaseRequested(staking_provider, value, 0, sender=creator) + new_value = minimum_authorization // 2 + + threshold_staking.setAuthorized(staking_provider, new_value, sender=creator) + tx = taco_application.resynchronizeAuthorization(staking_provider, sender=creator) + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == new_value + assert taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == new_value + assert taco_application.authorizedOverall() == new_value + assert taco_application.getOperatorFromStakingProvider(staking_provider) == staking_provider + assert taco_application.stakingProviderFromOperator(staking_provider) == staking_provider + assert taco_application.authorizedOverall() == new_value + assert taco_application.authorizedStake(staking_provider) == new_value + assert stake_info.authorizedStake(staking_provider) == 0 + assert taco_application.isAuthorized(staking_provider) + assert taco_application.isOperatorConfirmed(staking_provider) + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == staking_provider + + events = taco_application.AuthorizationReSynchronized.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == new_value + + # Set authorized amount to zero and resync again + value = new_value + threshold_staking.setAuthorized(staking_provider, 0, sender=creator) + tx = taco_application.resynchronizeAuthorization(staking_provider, sender=creator) + assert taco_application.stakingProviderInfo(staking_provider)[AUTHORIZATION_SLOT] == 0 + assert taco_application.stakingProviderInfo(staking_provider)[DEAUTHORIZING_SLOT] == 0 + assert taco_application.authorizedOverall() == 0 + assert taco_application.getOperatorFromStakingProvider(staking_provider) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(staking_provider) == ZERO_ADDRESS + assert taco_application.authorizedOverall() == 0 + assert taco_application.authorizedStake(staking_provider) == 0 + assert stake_info.authorizedStake(staking_provider) == 0 + assert not taco_application.isAuthorized(staking_provider) + assert not taco_application.isOperatorConfirmed(staking_provider) + assert not taco_application.stakingProviderInfo(staking_provider)[OPERATOR_CONFIRMED_SLOT] + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == ZERO_ADDRESS + + events = taco_application.AuthorizationReSynchronized.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["fromAmount"] == value + assert event["toAmount"] == 0 + + # Resync again without syncing with StakeInfo + value = minimum_authorization + threshold_staking.authorizationIncreased(staking_provider, 0, value, sender=creator) + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + taco_application.confirmOperatorAddress(sender=staking_provider) + threshold_staking.setAuthorized(staking_provider, 0, sender=creator) + taco_application.setUpdatableStakeInfo(ZERO_ADDRESS, sender=creator) + taco_application.resynchronizeAuthorization(staking_provider, sender=creator) + assert taco_application.authorizedStake(staking_provider) == 0 + assert stake_info.authorizedStake(staking_provider) == value + assert taco_application.getOperatorFromStakingProvider(staking_provider) == ZERO_ADDRESS + assert stake_info.stakes(staking_provider)[STAKE_INFO_OPERATOR_SLOT] == staking_provider diff --git a/tests/application/test_operator.py b/tests/application/test_operator.py index c6fcc511..7e3affa4 100644 --- a/tests/application/test_operator.py +++ b/tests/application/test_operator.py @@ -23,8 +23,10 @@ MIN_AUTHORIZATION = Web3.to_wei(40_000, "ether") MIN_OPERATOR_SECONDS = 24 * 60 * 60 +STAKE_INFO_OPERATOR_SLOT = 0 -def test_bond_operator(accounts, threshold_staking, pre_application, chain): + +def test_bond_operator(accounts, threshold_staking, taco_application, stake_info, chain): ( creator, staking_provider_1, @@ -41,79 +43,83 @@ def test_bond_operator(accounts, threshold_staking, pre_application, chain): min_authorization = MIN_AUTHORIZATION min_operator_seconds = MIN_OPERATOR_SECONDS - # Prepare staking providers: two with intermediary contract and two just a staking provider + # Prepare staking providers threshold_staking.setRoles(staking_provider_1, sender=creator) - threshold_staking.setStakes(staking_provider_1, min_authorization, 0, 0, sender=creator) - threshold_staking.setRoles(staking_provider_2, sender=creator) - threshold_staking.setStakes( - staking_provider_2, - min_authorization // 3, - min_authorization // 3, - min_authorization // 3 - 1, - sender=creator, + threshold_staking.authorizationIncreased( + staking_provider_1, 0, min_authorization, sender=creator ) + threshold_staking.setRoles(staking_provider_2, sender=creator) threshold_staking.setRoles(staking_provider_3, owner3, beneficiary, authorizer, sender=creator) - threshold_staking.setStakes(staking_provider_3, 0, min_authorization, 0, sender=creator) + threshold_staking.authorizationIncreased( + staking_provider_3, 0, min_authorization, sender=creator + ) threshold_staking.setRoles(staking_provider_4, sender=creator) - threshold_staking.setStakes(staking_provider_4, 0, 0, min_authorization, sender=creator) + threshold_staking.authorizationIncreased( + staking_provider_4, 0, min_authorization, sender=creator + ) - assert pre_application.getOperatorFromStakingProvider(staking_provider_1) == ZERO_ADDRESS - assert pre_application.stakingProviderFromOperator(staking_provider_1) == ZERO_ADDRESS - assert pre_application.getOperatorFromStakingProvider(staking_provider_2) == ZERO_ADDRESS - assert pre_application.stakingProviderFromOperator(staking_provider_2) == ZERO_ADDRESS - assert pre_application.getOperatorFromStakingProvider(staking_provider_3) == ZERO_ADDRESS - assert pre_application.stakingProviderFromOperator(staking_provider_3) == ZERO_ADDRESS - assert pre_application.getOperatorFromStakingProvider(staking_provider_4) == ZERO_ADDRESS - assert pre_application.stakingProviderFromOperator(staking_provider_4) == ZERO_ADDRESS + assert taco_application.getOperatorFromStakingProvider(staking_provider_1) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(staking_provider_1) == ZERO_ADDRESS + assert taco_application.getOperatorFromStakingProvider(staking_provider_2) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(staking_provider_2) == ZERO_ADDRESS + assert taco_application.getOperatorFromStakingProvider(staking_provider_3) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(staking_provider_3) == ZERO_ADDRESS + assert taco_application.getOperatorFromStakingProvider(staking_provider_4) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(staking_provider_4) == ZERO_ADDRESS # Staking provider can't confirm operator address because there is no operator by default with ape.reverts(): - pre_application.confirmOperatorAddress(sender=staking_provider_1) + taco_application.confirmOperatorAddress(sender=staking_provider_1) # Staking provider can't bond another staking provider as operator with ape.reverts(): - pre_application.bondOperator( + taco_application.bondOperator( staking_provider_1, staking_provider_2, sender=staking_provider_1 ) # Staking provider can't bond operator if stake is less than minimum with ape.reverts(): - pre_application.bondOperator(staking_provider_2, operator1, sender=staking_provider_2) + taco_application.bondOperator(staking_provider_2, operator1, sender=staking_provider_2) # Only staking provider or stake owner can bond operator with ape.reverts(): - pre_application.bondOperator(staking_provider_3, operator1, sender=beneficiary) + taco_application.bondOperator(staking_provider_3, operator1, sender=beneficiary) with ape.reverts(): - pre_application.bondOperator(staking_provider_3, operator1, sender=authorizer) + taco_application.bondOperator(staking_provider_3, operator1, sender=authorizer) # Staking provider bonds operator and now operator can make a confirmation - tx = pre_application.bondOperator(staking_provider_3, operator1, sender=owner3) + tx = taco_application.bondOperator(staking_provider_3, operator1, sender=owner3) timestamp = tx.timestamp - assert pre_application.getOperatorFromStakingProvider(staking_provider_3) == operator1 - assert pre_application.stakingProviderFromOperator(operator1) == staking_provider_3 - assert not pre_application.stakingProviderInfo(staking_provider_3)[CONFIRMATION_SLOT] - assert not pre_application.isOperatorConfirmed(operator1) - assert pre_application.getStakingProvidersLength() == 1 - assert pre_application.stakingProviders(0) == staking_provider_3 + assert taco_application.getOperatorFromStakingProvider(staking_provider_3) == operator1 + assert taco_application.stakingProviderFromOperator(operator1) == staking_provider_3 + assert not taco_application.stakingProviderInfo(staking_provider_3)[CONFIRMATION_SLOT] + assert not taco_application.isOperatorConfirmed(operator1) + assert taco_application.getStakingProvidersLength() == 1 + assert taco_application.stakingProviders(0) == staking_provider_3 + assert stake_info.stakes(staking_provider_3)[STAKE_INFO_OPERATOR_SLOT] == ZERO_ADDRESS + assert stake_info.stakingProviderFromOperator(operator1) == ZERO_ADDRESS # No active stakingProviders before confirmation - all_locked, staking_providers = pre_application.getActiveStakingProviders(0, 0) + all_locked, staking_providers = taco_application.getActiveStakingProviders(0, 0) assert all_locked == 0 assert len(staking_providers) == 0 - pre_application.confirmOperatorAddress(sender=operator1) - assert pre_application.stakingProviderInfo(staking_provider_3)[CONFIRMATION_SLOT] - assert pre_application.isOperatorConfirmed(operator1) + taco_application.confirmOperatorAddress(sender=operator1) + assert taco_application.stakingProviderInfo(staking_provider_3)[CONFIRMATION_SLOT] + assert taco_application.isOperatorConfirmed(operator1) + assert stake_info.stakes(staking_provider_3)[STAKE_INFO_OPERATOR_SLOT] == operator1 + assert stake_info.stakingProviderFromOperator(operator1) == staking_provider_3 - events = pre_application.OperatorBonded.from_receipt(tx) + events = taco_application.OperatorBonded.from_receipt(tx) assert len(events) == 1 event = events[0] assert event["stakingProvider"] == staking_provider_3 assert event["operator"] == operator1 + assert event["previousOperator"] == ZERO_ADDRESS assert event["startTimestamp"] == timestamp # After confirmation operator is becoming active - all_locked, staking_providers = pre_application.getActiveStakingProviders(0, 0) + all_locked, staking_providers = taco_application.getActiveStakingProviders(0, 0) assert all_locked == min_authorization assert len(staking_providers) == 1 assert to_checksum_address(staking_providers[0][0]) == staking_provider_3 @@ -121,208 +127,234 @@ def test_bond_operator(accounts, threshold_staking, pre_application, chain): # Operator is in use so other stakingProviders can't bond him with ape.reverts(): - pre_application.bondOperator(staking_provider_4, operator1, sender=staking_provider_4) + taco_application.bondOperator(staking_provider_4, operator1, sender=staking_provider_4) - # # Operator can't be a staking provider - # threshold_staking.setRoles(operator1, sender=creator) - # threshold_staking.setStakes(operator1, min_authorization, 0, 0, sender=creator) - # with ape.reverts(): - # threshold_staking.increaseAuthorization( - # operator1, min_authorization, pre_application.address, {'from': operator1}) + # Operator can't be a staking provider + threshold_staking.setRoles(operator1, sender=creator) + with ape.reverts(): + threshold_staking.authorizationIncreased(operator1, 0, min_authorization, sender=operator1) + threshold_staking.setRoles(operator1, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, sender=creator) # Can't bond operator twice too soon with ape.reverts(): - pre_application.bondOperator(staking_provider_3, operator2, sender=staking_provider_3) + taco_application.bondOperator(staking_provider_3, operator2, sender=staking_provider_3) # She can't unbond her operator too, until enough time has passed with ape.reverts(): - pre_application.bondOperator(staking_provider_3, ZERO_ADDRESS, sender=staking_provider_3) + taco_application.bondOperator(staking_provider_3, ZERO_ADDRESS, sender=staking_provider_3) # Let's advance some time and unbond the operator chain.pending_timestamp += min_operator_seconds - tx = pre_application.bondOperator(staking_provider_3, ZERO_ADDRESS, sender=staking_provider_3) + tx = taco_application.bondOperator(staking_provider_3, ZERO_ADDRESS, sender=staking_provider_3) timestamp = tx.timestamp - assert pre_application.getOperatorFromStakingProvider(staking_provider_3) == ZERO_ADDRESS - assert pre_application.stakingProviderFromOperator(staking_provider_3) == ZERO_ADDRESS - assert pre_application.stakingProviderFromOperator(operator1) == ZERO_ADDRESS - assert not pre_application.stakingProviderInfo(staking_provider_3)[CONFIRMATION_SLOT] - assert not pre_application.isOperatorConfirmed(operator1) - assert pre_application.getStakingProvidersLength() == 1 - assert pre_application.stakingProviders(0) == staking_provider_3 + assert taco_application.getOperatorFromStakingProvider(staking_provider_3) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(staking_provider_3) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(operator1) == ZERO_ADDRESS + assert not taco_application.stakingProviderInfo(staking_provider_3)[CONFIRMATION_SLOT] + assert not taco_application.isOperatorConfirmed(operator1) + assert taco_application.getStakingProvidersLength() == 1 + assert taco_application.stakingProviders(0) == staking_provider_3 + assert stake_info.stakes(staking_provider_3)[STAKE_INFO_OPERATOR_SLOT] == ZERO_ADDRESS + assert stake_info.stakingProviderFromOperator(operator1) == ZERO_ADDRESS # Resetting operator removes from active list before next confirmation - all_locked, staking_providers = pre_application.getActiveStakingProviders(0, 0) + all_locked, staking_providers = taco_application.getActiveStakingProviders(0, 0) assert all_locked == 0 assert len(staking_providers) == 0 - events = pre_application.OperatorBonded.from_receipt(tx) + events = taco_application.OperatorBonded.from_receipt(tx) assert len(events) == 1 event = events[0] assert event["stakingProvider"] == staking_provider_3 # Now the operator has been unbonded ... assert event["operator"] == ZERO_ADDRESS + assert event["previousOperator"] == operator1 # ... with a new starting period. assert event["startTimestamp"] == timestamp # The staking provider can bond now a new operator, without waiting additional time. - tx = pre_application.bondOperator(staking_provider_3, operator2, sender=staking_provider_3) + tx = taco_application.bondOperator(staking_provider_3, operator2, sender=staking_provider_3) timestamp = tx.timestamp - assert pre_application.getOperatorFromStakingProvider(staking_provider_3) == operator2 - assert pre_application.stakingProviderFromOperator(operator2) == staking_provider_3 - assert not pre_application.stakingProviderInfo(staking_provider_3)[CONFIRMATION_SLOT] - assert not pre_application.isOperatorConfirmed(operator2) - assert pre_application.getStakingProvidersLength() == 1 - assert pre_application.stakingProviders(0) == staking_provider_3 - - events = pre_application.OperatorBonded.from_receipt(tx) + assert taco_application.getOperatorFromStakingProvider(staking_provider_3) == operator2 + assert taco_application.stakingProviderFromOperator(operator2) == staking_provider_3 + assert not taco_application.stakingProviderInfo(staking_provider_3)[CONFIRMATION_SLOT] + assert not taco_application.isOperatorConfirmed(operator2) + assert taco_application.getStakingProvidersLength() == 1 + assert taco_application.stakingProviders(0) == staking_provider_3 + assert stake_info.stakes(staking_provider_3)[STAKE_INFO_OPERATOR_SLOT] == ZERO_ADDRESS + assert stake_info.stakingProviderFromOperator(operator2) == ZERO_ADDRESS + + events = taco_application.OperatorBonded.from_receipt(tx) assert len(events) == 1 event = events[0] assert event["stakingProvider"] == staking_provider_3 assert event["operator"] == operator2 + assert event["previousOperator"] == ZERO_ADDRESS assert event["startTimestamp"] == timestamp # Now the previous operator can no longer make a confirmation with ape.reverts(): - pre_application.confirmOperatorAddress(sender=operator1) + taco_application.confirmOperatorAddress(sender=operator1) # Only new operator can - pre_application.confirmOperatorAddress(sender=operator2) - assert not pre_application.isOperatorConfirmed(operator1) - assert pre_application.isOperatorConfirmed(operator2) - assert pre_application.stakingProviderInfo(staking_provider_3)[CONFIRMATION_SLOT] - - # Another staker can bond a free operator - tx = pre_application.bondOperator(staking_provider_4, operator1, sender=staking_provider_4) + taco_application.confirmOperatorAddress(sender=operator2) + assert not taco_application.isOperatorConfirmed(operator1) + assert taco_application.isOperatorConfirmed(operator2) + assert taco_application.stakingProviderInfo(staking_provider_3)[CONFIRMATION_SLOT] + assert stake_info.stakes(staking_provider_3)[STAKE_INFO_OPERATOR_SLOT] == operator2 + assert stake_info.stakingProviderFromOperator(operator2) == staking_provider_3 + + # Another staking provider can bond a free operator + assert taco_application.authorizedOverall() == min_authorization + tx = taco_application.bondOperator(staking_provider_4, operator1, sender=staking_provider_4) timestamp = tx.timestamp - assert pre_application.getOperatorFromStakingProvider(staking_provider_4) == operator1 - assert pre_application.stakingProviderFromOperator(operator1) == staking_provider_4 - assert not pre_application.isOperatorConfirmed(operator1) - assert not pre_application.stakingProviderInfo(staking_provider_4)[CONFIRMATION_SLOT] - assert pre_application.getStakingProvidersLength() == 2 - assert pre_application.stakingProviders(1) == staking_provider_4 - - events = pre_application.OperatorBonded.from_receipt(tx) + assert taco_application.getOperatorFromStakingProvider(staking_provider_4) == operator1 + assert taco_application.stakingProviderFromOperator(operator1) == staking_provider_4 + assert not taco_application.isOperatorConfirmed(operator1) + assert not taco_application.stakingProviderInfo(staking_provider_4)[CONFIRMATION_SLOT] + assert taco_application.getStakingProvidersLength() == 2 + assert taco_application.stakingProviders(1) == staking_provider_4 + assert taco_application.authorizedOverall() == min_authorization + assert stake_info.stakes(staking_provider_4)[STAKE_INFO_OPERATOR_SLOT] == ZERO_ADDRESS + assert stake_info.stakingProviderFromOperator(operator1) == ZERO_ADDRESS + + events = taco_application.OperatorBonded.from_receipt(tx) assert len(events) == 1 event = events[0] assert event["stakingProvider"] == staking_provider_4 assert event["operator"] == operator1 + assert event["previousOperator"] == ZERO_ADDRESS assert event["startTimestamp"] == timestamp - # # The first operator still can't be a staking provider - # threshold_staking.setRoles(operator1, sender=creator) - # threshold_staking.setStakes(operator1, min_authorization, 0, 0, sender=creator) - # with ape.reverts(): - # threshold_staking.increaseAuthorization( - # operator1, min_authorization, pre_application.address, {'from': operator1}) + # The first operator still can't be a staking provider + threshold_staking.setRoles(operator1, sender=creator) + with ape.reverts(): + threshold_staking.authorizationIncreased(operator1, 0, min_authorization, sender=operator1) + threshold_staking.setRoles(operator1, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, sender=creator) # Bond operator again - pre_application.confirmOperatorAddress(sender=operator1) - assert pre_application.isOperatorConfirmed(operator1) - assert pre_application.stakingProviderInfo(staking_provider_4)[CONFIRMATION_SLOT] + taco_application.confirmOperatorAddress(sender=operator1) + assert taco_application.isOperatorConfirmed(operator1) + assert taco_application.stakingProviderInfo(staking_provider_4)[CONFIRMATION_SLOT] + assert taco_application.authorizedOverall() == 2 * min_authorization + assert stake_info.stakes(staking_provider_4)[STAKE_INFO_OPERATOR_SLOT] == operator1 + assert stake_info.stakingProviderFromOperator(operator1) == staking_provider_4 + chain.pending_timestamp += min_operator_seconds - tx = pre_application.bondOperator(staking_provider_4, operator3, sender=staking_provider_4) + tx = taco_application.bondOperator(staking_provider_4, operator3, sender=staking_provider_4) timestamp = tx.timestamp - assert pre_application.getOperatorFromStakingProvider(staking_provider_4) == operator3 - assert pre_application.stakingProviderFromOperator(operator3) == staking_provider_4 - assert pre_application.stakingProviderFromOperator(operator1) == ZERO_ADDRESS - assert not pre_application.isOperatorConfirmed(operator3) - assert not pre_application.isOperatorConfirmed(operator1) - assert not pre_application.stakingProviderInfo(staking_provider_4)[CONFIRMATION_SLOT] - assert pre_application.getStakingProvidersLength() == 2 - assert pre_application.stakingProviders(1) == staking_provider_4 + assert taco_application.getOperatorFromStakingProvider(staking_provider_4) == operator3 + assert taco_application.stakingProviderFromOperator(operator3) == staking_provider_4 + assert taco_application.stakingProviderFromOperator(operator1) == ZERO_ADDRESS + assert not taco_application.isOperatorConfirmed(operator3) + assert not taco_application.isOperatorConfirmed(operator1) + assert not taco_application.stakingProviderInfo(staking_provider_4)[CONFIRMATION_SLOT] + assert taco_application.getStakingProvidersLength() == 2 + assert taco_application.stakingProviders(1) == staking_provider_4 + assert taco_application.authorizedOverall() == min_authorization + assert stake_info.stakes(staking_provider_4)[STAKE_INFO_OPERATOR_SLOT] == ZERO_ADDRESS + assert stake_info.stakingProviderFromOperator(operator1) == ZERO_ADDRESS # Resetting operator removes from active list before next confirmation - all_locked, staking_providers = pre_application.getActiveStakingProviders(1, 0) + all_locked, staking_providers = taco_application.getActiveStakingProviders(1, 0) assert all_locked == 0 assert len(staking_providers) == 0 - events = pre_application.OperatorBonded.from_receipt(tx) + events = taco_application.OperatorBonded.from_receipt(tx) assert len(events) == 1 event = events[0] assert event["stakingProvider"] == staking_provider_4 assert event["operator"] == operator3 + assert event["previousOperator"] == operator1 assert event["startTimestamp"] == timestamp - # The first operator is free and can deposit tokens and become a staker + # The first operator is free and can deposit tokens and become a staking provider threshold_staking.setRoles(operator1, sender=creator) - threshold_staking.setStakes( - operator1, - min_authorization // 3, - min_authorization // 3, - min_authorization // 3, - sender=creator, - ) - # threshold_staking.increaseAuthorization( - # operator1, min_authorization, pre_application.address, {'from': operator1}) - assert pre_application.getOperatorFromStakingProvider(operator1) == ZERO_ADDRESS - assert pre_application.stakingProviderFromOperator(operator1) == ZERO_ADDRESS + threshold_staking.authorizationIncreased(operator1, 0, min_authorization, sender=operator1) + assert taco_application.getOperatorFromStakingProvider(operator1) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(operator1) == ZERO_ADDRESS chain.pending_timestamp += min_operator_seconds # Staking provider can't bond the first operator again because operator is a provider now with ape.reverts(): - pre_application.bondOperator(staking_provider_4, operator1, sender=staking_provider_4) + taco_application.bondOperator(staking_provider_4, operator1, sender=staking_provider_4) # Provider without intermediary contract can bond itself as operator # (Probably not best idea, but whatever) - tx = pre_application.bondOperator( + tx = taco_application.bondOperator( staking_provider_1, staking_provider_1, sender=staking_provider_1 ) timestamp = tx.timestamp - assert pre_application.getOperatorFromStakingProvider(staking_provider_1) == staking_provider_1 - assert pre_application.stakingProviderFromOperator(staking_provider_1) == staking_provider_1 - assert pre_application.getStakingProvidersLength() == 3 - assert pre_application.stakingProviders(2) == staking_provider_1 + assert taco_application.getOperatorFromStakingProvider(staking_provider_1) == staking_provider_1 + assert taco_application.stakingProviderFromOperator(staking_provider_1) == staking_provider_1 + assert taco_application.getStakingProvidersLength() == 3 + assert taco_application.stakingProviders(2) == staking_provider_1 - events = pre_application.OperatorBonded.from_receipt(tx) + events = taco_application.OperatorBonded.from_receipt(tx) assert len(events) == 1 event = events[0] assert event["stakingProvider"] == staking_provider_1 assert event["operator"] == staking_provider_1 + assert event["previousOperator"] == ZERO_ADDRESS assert event["startTimestamp"] == timestamp - # If stake will be less than minimum then confirmation is not possible - threshold_staking.setStakes(staking_provider_1, 0, min_authorization - 1, 0, sender=creator) - - with ape.reverts(): - pre_application.confirmOperatorAddress(sender=staking_provider_1) - - # Now provider can make a confirmation - threshold_staking.setStakes(staking_provider_1, 0, 0, min_authorization, sender=creator) - pre_application.confirmOperatorAddress(sender=staking_provider_1) + # If stake will be less than minimum then confirmation is still possible + threshold_staking.involuntaryAuthorizationDecrease( + staking_provider_1, min_authorization, min_authorization - 1, sender=creator + ) + taco_application.confirmOperatorAddress(sender=staking_provider_1) + assert stake_info.stakes(staking_provider_1)[STAKE_INFO_OPERATOR_SLOT] == staking_provider_1 + assert stake_info.stakingProviderFromOperator(staking_provider_1) == staking_provider_1 # If stake will be less than minimum then provider is not active - all_locked, staking_providers = pre_application.getActiveStakingProviders(0, 0) + threshold_staking.authorizationIncreased( + staking_provider_1, min_authorization - 1, min_authorization, sender=creator + ) + all_locked, staking_providers = taco_application.getActiveStakingProviders(0, 0) assert all_locked == 2 * min_authorization assert len(staking_providers) == 2 assert to_checksum_address(staking_providers[0][0]) == staking_provider_3 assert staking_providers[0][1] == min_authorization assert to_checksum_address(staking_providers[1][0]) == staking_provider_1 assert staking_providers[1][1] == min_authorization - threshold_staking.setStakes(staking_provider_1, 0, min_authorization - 1, 0, sender=creator) - all_locked, staking_providers = pre_application.getActiveStakingProviders(1, 0) + threshold_staking.involuntaryAuthorizationDecrease( + staking_provider_1, min_authorization, min_authorization - 1, sender=creator + ) + all_locked, staking_providers = taco_application.getActiveStakingProviders(1, 0) assert all_locked == 0 assert len(staking_providers) == 0 + # Reset xchain contract before next bonding + taco_application.setUpdatableStakeInfo(ZERO_ADDRESS, sender=creator) + + # Unbond and rebond oeprator + taco_application.bondOperator(staking_provider_3, ZERO_ADDRESS, sender=staking_provider_3) + taco_application.bondOperator(staking_provider_3, operator2, sender=staking_provider_3) + assert not taco_application.isOperatorConfirmed(operator2) -def test_confirm_address(accounts, threshold_staking, pre_application, chain, project): + # Operator can be unbonded before confirmation without restriction + taco_application.bondOperator(staking_provider_3, ZERO_ADDRESS, sender=staking_provider_3) + assert taco_application.getOperatorFromStakingProvider(staking_provider_3) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(staking_provider_3) == ZERO_ADDRESS + assert taco_application.stakingProviderFromOperator(operator2) == ZERO_ADDRESS + + +def test_confirm_address(accounts, threshold_staking, taco_application, chain, project): creator, staking_provider, operator, *everyone_else = accounts[0:] min_authorization = MIN_AUTHORIZATION min_operator_seconds = MIN_OPERATOR_SECONDS - # Operator must be associated with provider that has minimum amount of tokens + # Operator must be associated with staking provider with ape.reverts(): - pre_application.confirmOperatorAddress(sender=staking_provider) + taco_application.confirmOperatorAddress(sender=staking_provider) threshold_staking.setRoles(staking_provider, sender=creator) - threshold_staking.setStakes(staking_provider, min_authorization - 1, 0, 0, sender=creator) - with ape.reverts(): - pre_application.confirmOperatorAddress(sender=staking_provider) # Deploy intermediary contract - intermediary = creator.deploy(project.Intermediary, pre_application.address, sender=creator) + intermediary = creator.deploy(project.Intermediary, taco_application.address, sender=creator) # Bond contract as an operator - threshold_staking.setStakes(staking_provider, min_authorization, 0, 0, sender=creator) - pre_application.bondOperator(staking_provider, intermediary.address, sender=staking_provider) + threshold_staking.authorizationIncreased(staking_provider, 0, min_authorization, sender=creator) + taco_application.bondOperator(staking_provider, intermediary.address, sender=staking_provider) # But can't make a confirmation using an intermediary contract with ape.reverts(): @@ -330,12 +362,14 @@ def test_confirm_address(accounts, threshold_staking, pre_application, chain, pr # Bond operator again and make confirmation chain.pending_timestamp += min_operator_seconds - pre_application.bondOperator(staking_provider, operator, sender=staking_provider) - tx = pre_application.confirmOperatorAddress(sender=operator) - assert pre_application.isOperatorConfirmed(operator) - assert pre_application.stakingProviderInfo(staking_provider)[CONFIRMATION_SLOT] - - events = pre_application.OperatorConfirmed.from_receipt(tx) + taco_application.bondOperator(staking_provider, operator, sender=staking_provider) + assert taco_application.authorizedOverall() == 0 + tx = taco_application.confirmOperatorAddress(sender=operator) + assert taco_application.isOperatorConfirmed(operator) + assert taco_application.stakingProviderInfo(staking_provider)[CONFIRMATION_SLOT] + assert taco_application.authorizedOverall() == min_authorization + + events = taco_application.OperatorConfirmed.from_receipt(tx) assert len(events) == 1 event = events[0] assert event["stakingProvider"] == staking_provider @@ -343,4 +377,18 @@ def test_confirm_address(accounts, threshold_staking, pre_application, chain, pr # Can't confirm twice with ape.reverts(): - pre_application.confirmOperatorAddress(sender=operator) + taco_application.confirmOperatorAddress(sender=operator) + + +def test_slash(accounts, threshold_staking, taco_application): + creator, staking_provider, investigator, *everyone_else = accounts[0:] + min_authorization = MIN_AUTHORIZATION + penalty = min_authorization + + taco_application.setAdjudicator(creator, sender=creator) + taco_application.slash(staking_provider, penalty, investigator, sender=creator) + assert threshold_staking.amountToSeize() == penalty + assert threshold_staking.rewardMultiplier() == 100 + assert threshold_staking.notifier() == investigator + assert threshold_staking.stakingProvidersToSeize(0) == staking_provider + assert threshold_staking.getLengthOfStakingProvidersToSeize() == 1 diff --git a/tests/application/test_reward.py b/tests/application/test_reward.py new file mode 100644 index 00000000..df2ad775 --- /dev/null +++ b/tests/application/test_reward.py @@ -0,0 +1,445 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + +import ape +from ape.utils import ZERO_ADDRESS +from web3 import Web3 + +REWARDS_SLOT = 6 +REWARDS_PAID_SLOT = 7 +ERROR = 1e5 +MIN_AUTHORIZATION = Web3.to_wei(40_000, "ether") +MIN_OPERATOR_SECONDS = 24 * 60 * 60 +REWARD_DURATION = 60 * 60 * 24 * 7 # one week in seconds +DEAUTHORIZATION_DURATION = 60 * 60 * 24 * 60 # 60 days in seconds + + +def test_push_reward(accounts, token, threshold_staking, taco_application, chain): + creator, distributor, staking_provider_1, staking_provider_2, *everyone_else = accounts[0:] + min_authorization = MIN_AUTHORIZATION + reward_portion = min_authorization + reward_duration = REWARD_DURATION + value = int(1.5 * min_authorization) + + # Can't push reward without distributor + token.approve(taco_application.address, reward_portion, sender=creator) + with ape.reverts(): + taco_application.pushReward(reward_portion, sender=creator) + + # Only owner can set distributor + with ape.reverts(): + taco_application.setRewardDistributor(distributor, sender=distributor) + + tx = taco_application.setRewardDistributor(distributor, sender=creator) + assert taco_application.rewardDistributor() == distributor + + events = taco_application.RewardDistributorSet.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["distributor"] == distributor + + # Can't distribute zero rewards + with ape.reverts(): + taco_application.pushReward(0, sender=distributor) + + # Push reward without staking providers + token.transfer(distributor, 10 * reward_portion, sender=creator) + token.approve(taco_application.address, 10 * reward_portion, sender=distributor) + tx = taco_application.pushReward(reward_portion, sender=distributor) + timestamp = chain.pending_timestamp - 1 + assert taco_application.rewardRateDecimals() == reward_portion * 10**18 // reward_duration + assert taco_application.lastUpdateTime() == timestamp + assert taco_application.periodFinish() == (timestamp + reward_duration) + assert token.balanceOf(taco_application.address) == reward_portion + assert token.balanceOf(distributor) == 9 * reward_portion + assert taco_application.lastTimeRewardApplicable() == timestamp + assert taco_application.rewardPerTokenStored() == 0 + assert taco_application.rewardPerToken() == 0 + assert taco_application.availableRewards(staking_provider_1) == 0 + + events = taco_application.RewardAdded.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["reward"] == reward_portion + + # Wait some time and push reward again (without staking providers) + chain.pending_timestamp += reward_duration // 2 - 1 + tx = taco_application.pushReward(reward_portion, sender=distributor) + timestamp = chain.pending_timestamp - 1 + expected_reward_rate = (reward_portion + reward_portion // 2) * 10**18 // reward_duration + # Could be some error during calculations + assert abs(taco_application.rewardRateDecimals() - expected_reward_rate) <= ERROR + assert taco_application.lastUpdateTime() == timestamp + assert taco_application.periodFinish() == (timestamp + reward_duration) + assert token.balanceOf(taco_application.address) == 2 * reward_portion + assert token.balanceOf(distributor) == 8 * reward_portion + assert taco_application.lastTimeRewardApplicable() == timestamp + assert taco_application.rewardPerTokenStored() == 0 + assert taco_application.rewardPerToken() == 0 + assert taco_application.availableRewards(staking_provider_1) == 0 + + events = taco_application.RewardAdded.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["reward"] == reward_portion + + # Wait, add one staking provider and push reward again + chain.pending_timestamp += reward_duration + threshold_staking.authorizationIncreased(staking_provider_1, 0, value, sender=creator) + taco_application.bondOperator(staking_provider_1, staking_provider_1, sender=staking_provider_1) + taco_application.confirmOperatorAddress(sender=staking_provider_1) + + tx = taco_application.pushReward(reward_portion, sender=distributor) + timestamp = chain.pending_timestamp - 1 + assert taco_application.rewardRateDecimals() == reward_portion * 10**18 // reward_duration + assert taco_application.lastUpdateTime() == timestamp + assert taco_application.periodFinish() == (timestamp + reward_duration) + assert token.balanceOf(taco_application.address) == 3 * reward_portion + assert token.balanceOf(distributor) == 7 * reward_portion + assert taco_application.lastTimeRewardApplicable() == timestamp + assert taco_application.rewardPerTokenStored() == 0 + assert taco_application.rewardPerToken() == 0 + assert taco_application.availableRewards(staking_provider_1) == 0 + + events = taco_application.RewardAdded.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["reward"] == reward_portion + + # Wait some time and check reward for staking provider + chain.pending_timestamp += reward_duration // 2 + assert taco_application.rewardPerTokenStored() == 0 + expected_reward_per_token = int(reward_portion * 1e18) // value // 2 + assert abs(taco_application.rewardPerToken() - expected_reward_per_token) < ERROR + expected_reward = reward_portion // 2 + assert abs(taco_application.availableRewards(staking_provider_1) - expected_reward) < ERROR + + chain.pending_timestamp += reward_duration // 2 + assert taco_application.rewardPerTokenStored() == 0 + expected_reward_per_token = int(reward_portion * 1e18) // value + reward_per_token = taco_application.rewardPerToken() + assert abs(reward_per_token - expected_reward_per_token) <= 100 + expected_reward = reward_portion + reward = taco_application.availableRewards(staking_provider_1) + assert abs(reward - expected_reward) <= ERROR + + # Add another staking provider without confirmation and push reward again + threshold_staking.authorizationIncreased(staking_provider_2, 0, value, sender=creator) + tx = taco_application.pushReward(reward_portion, sender=distributor) + timestamp = chain.pending_timestamp - 1 + assert taco_application.rewardRateDecimals() == reward_portion * 10**18 // reward_duration + assert taco_application.lastUpdateTime() == timestamp + assert taco_application.periodFinish() == (timestamp + reward_duration) + assert token.balanceOf(taco_application.address) == 4 * reward_portion + assert token.balanceOf(distributor) == 6 * reward_portion + assert taco_application.lastTimeRewardApplicable() == timestamp + assert taco_application.rewardPerTokenStored() == reward_per_token + assert taco_application.rewardPerToken() == reward_per_token + assert taco_application.availableRewards(staking_provider_1) == reward + assert taco_application.availableRewards(staking_provider_2) == 0 + + events = taco_application.RewardAdded.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["reward"] == reward_portion + + chain.pending_timestamp += reward_duration + assert ( + abs(taco_application.availableRewards(staking_provider_1) - (reward + reward_portion)) + < ERROR + ) + assert taco_application.availableRewards(staking_provider_2) == 0 + + +def test_update_reward(accounts, token, threshold_staking, taco_application, chain): + creator, distributor, staking_provider_1, staking_provider_2, *everyone_else = accounts[0:] + min_authorization = MIN_AUTHORIZATION + reward_portion = min_authorization + reward_duration = REWARD_DURATION + deauthorization_duration = DEAUTHORIZATION_DURATION + min_operator_seconds = MIN_OPERATOR_SECONDS + value = int(1.5 * min_authorization) + + reward_per_token = 0 + new_reward_per_token = 0 + staking_provider_1_reward = 0 + staking_provider_1_new_reward = 0 + staking_provider_2_reward = 0 + staking_provider_2_new_reward = 0 + + def check_reward_no_confirmation(): + nonlocal reward_per_token, new_reward_per_token + nonlocal staking_provider_1_reward, staking_provider_1_new_reward + + new_reward_per_token = taco_application.rewardPerToken() + assert new_reward_per_token > reward_per_token + assert taco_application.rewardPerTokenStored() == new_reward_per_token + staking_provider_1_new_reward = taco_application.availableRewards(staking_provider_1) + assert staking_provider_1_new_reward > staking_provider_1_reward + assert taco_application.stakingProviderInfo(staking_provider_1)[REWARDS_SLOT] == 0 + assert taco_application.stakingProviderInfo(staking_provider_1)[REWARDS_PAID_SLOT] == 0 + assert taco_application.availableRewards(staking_provider_2) == 0 + assert taco_application.stakingProviderInfo(staking_provider_2)[REWARDS_SLOT] == 0 + assert ( + taco_application.stakingProviderInfo(staking_provider_2)[REWARDS_PAID_SLOT] + == new_reward_per_token + ) + reward_per_token = new_reward_per_token + staking_provider_1_reward = staking_provider_1_new_reward + + def check_reward_with_confirmation(): + nonlocal reward_per_token, new_reward_per_token, staking_provider_1_reward + nonlocal staking_provider_1_new_reward, staking_provider_2_reward + nonlocal staking_provider_2_new_reward + + new_reward_per_token = taco_application.rewardPerToken() + assert new_reward_per_token > reward_per_token + assert taco_application.rewardPerTokenStored() == new_reward_per_token + staking_provider_1_new_reward = taco_application.availableRewards(staking_provider_1) + assert staking_provider_1_new_reward > staking_provider_1_reward + assert taco_application.stakingProviderInfo(staking_provider_1)[REWARDS_SLOT] == 0 + assert taco_application.stakingProviderInfo(staking_provider_1)[REWARDS_PAID_SLOT] == 0 + staking_provider_2_new_reward = taco_application.availableRewards(staking_provider_2) + assert staking_provider_2_new_reward > staking_provider_2_reward + assert ( + taco_application.stakingProviderInfo(staking_provider_2)[REWARDS_SLOT] + == staking_provider_2_new_reward + ) + assert ( + taco_application.stakingProviderInfo(staking_provider_2)[REWARDS_PAID_SLOT] + == new_reward_per_token + ) + reward_per_token = new_reward_per_token + staking_provider_1_reward = staking_provider_1_new_reward + staking_provider_2_reward = staking_provider_2_new_reward + + # Prepare one staking provider and reward + threshold_staking.authorizationIncreased(staking_provider_1, 0, value, sender=creator) + taco_application.bondOperator(staking_provider_1, staking_provider_1, sender=staking_provider_1) + taco_application.confirmOperatorAddress(sender=staking_provider_1) + + taco_application.setRewardDistributor(distributor, sender=creator) + token.transfer(distributor, 100 * reward_portion, sender=creator) + token.approve(taco_application.address, 100 * reward_portion, sender=distributor) + taco_application.pushReward(2 * reward_portion, sender=distributor) + assert taco_application.rewardPerTokenStored() == 0 + assert taco_application.rewardPerToken() == 0 + assert taco_application.availableRewards(staking_provider_1) == 0 + + chain.pending_timestamp += reward_duration // 2 + # Reward per token will be updated but nothing earned yet + threshold_staking.authorizationIncreased(staking_provider_2, 0, 4 * value, sender=creator) + check_reward_no_confirmation() + + # Add reward, wait and bond operator + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration // 2 + # Reward per token will be updated but nothing earned yet (need confirmation) + taco_application.bondOperator(staking_provider_2, staking_provider_2, sender=staking_provider_2) + check_reward_no_confirmation() + + # Involuntary decrease without confirmation + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration // 2 + threshold_staking.involuntaryAuthorizationDecrease( + staking_provider_2, 4 * value, 3 * value, sender=creator + ) + check_reward_no_confirmation() + + # Request for decrease + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration // 2 + threshold_staking.authorizationDecreaseRequested( + staking_provider_2, 3 * value, 2 * value, sender=creator + ) + check_reward_no_confirmation() + + # Finish decrease without confirmation + chain.pending_timestamp += deauthorization_duration + taco_application.finishAuthorizationDecrease(staking_provider_2, sender=creator) + check_reward_no_confirmation() + + # Resync without confirmation + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration // 2 + threshold_staking.setAuthorized(staking_provider_2, value, sender=creator) + taco_application.resynchronizeAuthorization(staking_provider_2, sender=creator) + check_reward_no_confirmation() + + # Wait and confirm operator + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration // 2 + # Reward per token will be updated but nothing earned yet (just confirmed operator) + taco_application.confirmOperatorAddress(sender=staking_provider_2) + check_reward_no_confirmation() + + # Increase authorization with confirmation + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration // 2 + threshold_staking.authorizationIncreased(staking_provider_2, value, 4 * value, sender=creator) + check_reward_with_confirmation() + + # Involuntary decrease with confirmation + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration // 2 + threshold_staking.involuntaryAuthorizationDecrease( + staking_provider_2, 4 * value, 3 * value, sender=creator + ) + check_reward_with_confirmation() + + # Request for decrease + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration // 2 + threshold_staking.authorizationDecreaseRequested( + staking_provider_2, 3 * value, 2 * value, sender=creator + ) + check_reward_with_confirmation() + + # Finish decrease with confirmation + chain.pending_timestamp += deauthorization_duration + taco_application.finishAuthorizationDecrease(staking_provider_2, sender=creator) + check_reward_with_confirmation() + + # Resync with confirmation + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration // 2 + threshold_staking.setAuthorized(staking_provider_2, value, sender=creator) + taco_application.resynchronizeAuthorization(staking_provider_2, sender=creator) + check_reward_with_confirmation() + + # Bond operator with confirmation (confirmation will be dropped) + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += min_operator_seconds + # Reward per token will be updated but nothing earned yet (need confirmation) + taco_application.bondOperator(staking_provider_2, everyone_else[0], sender=staking_provider_2) + check_reward_with_confirmation() + + # Push reward wait some time and check that no more reward + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration + assert taco_application.availableRewards(staking_provider_2) == staking_provider_2_reward + assert ( + taco_application.stakingProviderInfo(staking_provider_2)[REWARDS_SLOT] + == staking_provider_2_reward + ) + assert ( + taco_application.stakingProviderInfo(staking_provider_2)[REWARDS_PAID_SLOT] + == reward_per_token + ) + + +def test_withdraw(accounts, token, threshold_staking, taco_application, chain): + ( + creator, + distributor, + staking_provider, + owner, + beneficiary, + authorizer, + staking_provider_2, + *everyone_else, + ) = accounts[0:] + min_authorization = MIN_AUTHORIZATION + reward_portion = min_authorization + reward_duration = REWARD_DURATION + min_operator_seconds = MIN_OPERATOR_SECONDS + value = int(1.5 * min_authorization) + + # No rewards, no staking providers + threshold_staking.setRoles(staking_provider, owner, beneficiary, authorizer, sender=creator) + with ape.reverts(): + taco_application.withdrawRewards(staking_provider, sender=beneficiary) + + # Prepare one staking provider and reward + threshold_staking.authorizationIncreased(staking_provider, 0, value, sender=creator) + taco_application.bondOperator(staking_provider, staking_provider, sender=staking_provider) + taco_application.confirmOperatorAddress(sender=staking_provider) + + # Nothing earned yet + with ape.reverts(): + taco_application.withdrawRewards(staking_provider, sender=beneficiary) + + taco_application.setRewardDistributor(distributor, sender=creator) + token.transfer(distributor, 100 * reward_portion, sender=creator) + token.approve(taco_application.address, 100 * reward_portion, sender=distributor) + taco_application.pushReward(reward_portion, sender=distributor) + assert taco_application.rewardPerTokenStored() == 0 + assert taco_application.rewardPerToken() == 0 + assert taco_application.availableRewards(staking_provider) == 0 + + chain.pending_timestamp += reward_duration + # Only beneficiary can withdraw reward + with ape.reverts(): + taco_application.withdrawRewards(staking_provider, sender=owner) + with ape.reverts(): + taco_application.withdrawRewards(staking_provider, sender=authorizer) + + reward_per_token = taco_application.rewardPerToken() + assert reward_per_token > 0 + earned = taco_application.availableRewards(staking_provider) + assert earned > 0 + + tx = taco_application.withdrawRewards(staking_provider, sender=beneficiary) + assert taco_application.rewardPerTokenStored() == reward_per_token + assert taco_application.stakingProviderInfo(staking_provider)[REWARDS_SLOT] == 0 + assert ( + taco_application.stakingProviderInfo(staking_provider)[REWARDS_PAID_SLOT] + == reward_per_token + ) + assert token.balanceOf(beneficiary) == earned + assert token.balanceOf(taco_application.address) == reward_portion - earned + + events = taco_application.RewardPaid.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["beneficiary"] == beneficiary + assert event["reward"] == earned + + # Add one more staking provider, push reward again and drop operator + chain.pending_timestamp += min_operator_seconds + threshold_staking.setRoles(staking_provider_2, sender=creator) + threshold_staking.authorizationIncreased(staking_provider_2, 0, value, sender=creator) + taco_application.bondOperator(staking_provider_2, staking_provider_2, sender=staking_provider_2) + taco_application.confirmOperatorAddress(sender=staking_provider_2) + taco_application.pushReward(reward_portion, sender=distributor) + chain.pending_timestamp += reward_duration // 2 + taco_application.bondOperator(staking_provider, ZERO_ADDRESS, sender=staking_provider) + + new_earned = taco_application.availableRewards(staking_provider) + assert taco_application.stakingProviderInfo(staking_provider)[REWARDS_SLOT] == new_earned + + # Withdraw + chain.pending_timestamp += reward_duration // 2 + assert taco_application.availableRewards(staking_provider) == new_earned + tx = taco_application.withdrawRewards(staking_provider, sender=beneficiary) + new_reward_per_token = taco_application.rewardPerToken() + assert taco_application.rewardPerTokenStored() == new_reward_per_token + assert taco_application.stakingProviderInfo(staking_provider)[REWARDS_SLOT] == 0 + assert ( + taco_application.stakingProviderInfo(staking_provider)[REWARDS_PAID_SLOT] + == new_reward_per_token + ) + assert token.balanceOf(beneficiary) == earned + new_earned + assert token.balanceOf(taco_application.address) == 2 * reward_portion - earned - new_earned + + events = taco_application.RewardPaid.from_receipt(tx) + assert len(events) == 1 + event = events[0] + assert event["stakingProvider"] == staking_provider + assert event["beneficiary"] == beneficiary + assert event["reward"] == new_earned