// SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.8.0; import {IERC20} from './interfaces/IERC20.sol'; import {IStakedTokenV2} from './interfaces/IStakedTokenV2.sol'; import {ERC20} from './lib/ERC20.sol'; import {DistributionTypes} from './lib/DistributionTypes.sol'; import {SafeERC20} from './lib/SafeERC20.sol'; import {VersionedInitializable} from './utils/VersionedInitializable.sol'; import {PegasysDistributionManager} from './PegasysDistributionManager.sol'; import {GovernancePowerWithSnapshot} from './lib/GovernancePowerWithSnapshot.sol'; /** * @title StakedTokenV2 * @notice Contract to stake Aave token, tokenize the position and get rewards, inheriting from a distribution manager contract * @author BGD Labs */ abstract contract StakedTokenV2 is IStakedTokenV2, GovernancePowerWithSnapshot, VersionedInitializable, PegasysDistributionManager { using SafeERC20 for IERC20; IERC20 public immutable STAKED_TOKEN; IERC20 public immutable REWARD_TOKEN; /// @notice Seconds available to redeem once the cooldown period is fulfilled uint256 public immutable UNSTAKE_WINDOW; /// @notice Address to pull from the rewards, needs to have approved this contract address public immutable REWARDS_VAULT; mapping(address => uint256) public stakerRewardsToClaim; mapping(address => CooldownSnapshot) public stakersCooldowns; /// @dev End of Storage layout from StakedToken v1 /// @dev To see the voting mappings, go to GovernancePowerWithSnapshot.sol mapping(address => address) internal _votingDelegates; mapping(address => mapping(uint256 => Snapshot)) internal _propositionPowerSnapshots; mapping(address => uint256) internal _propositionPowerSnapshotsCounts; mapping(address => address) internal _propositionPowerDelegates; bytes32 public DOMAIN_SEPARATOR; bytes public constant EIP712_REVISION = bytes('1'); bytes32 internal constant EIP712_DOMAIN = keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' ); bytes32 public constant PERMIT_TYPEHASH = keccak256( 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)' ); /// @dev owner => next valid nonce to submit with permit() mapping(address => uint256) public _nonces; constructor( IERC20 stakedToken, IERC20 rewardToken, uint256 unstakeWindow, address rewardsVault, address emissionManager, uint128 distributionDuration ) ERC20() PegasysDistributionManager(emissionManager, distributionDuration) { STAKED_TOKEN = stakedToken; REWARD_TOKEN = rewardToken; UNSTAKE_WINDOW = unstakeWindow; REWARDS_VAULT = rewardsVault; } /// @inheritdoc IStakedTokenV2 function stake(address onBehalfOf, uint256 amount) external virtual override; /// @inheritdoc IStakedTokenV2 function redeem(address to, uint256 amount) external virtual override; /// @inheritdoc IStakedTokenV2 function cooldown() external virtual override; /// @inheritdoc IStakedTokenV2 function claimRewards(address to, uint256 amount) external virtual override; /// @inheritdoc IStakedTokenV2 function getTotalRewardsBalance(address staker) external view returns (uint256) { DistributionTypes.UserStakeInput[] memory userStakeInputs = new DistributionTypes.UserStakeInput[](1); userStakeInputs[0] = DistributionTypes.UserStakeInput({ underlyingAsset: address(this), stakedByUser: balanceOf(staker), totalStaked: totalSupply() }); return stakerRewardsToClaim[staker] + _getUnclaimedRewards(staker, userStakeInputs); } /// @inheritdoc IStakedTokenV2 function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { require(owner != address(0), 'INVALID_OWNER'); //solium-disable-next-line require(block.timestamp <= deadline, 'INVALID_EXPIRATION'); uint256 currentValidNonce = _nonces[owner]; bytes32 digest = keccak256( abi.encodePacked( '\x19\x01', DOMAIN_SEPARATOR, keccak256( abi.encode( PERMIT_TYPEHASH, owner, spender, value, currentValidNonce, deadline ) ) ) ); require(owner == ecrecover(digest, v, r, s), 'INVALID_SIGNATURE'); unchecked { _nonces[owner] = currentValidNonce + 1; } _approve(owner, spender, value); } /** * @dev Delegates power from signatory to `delegatee` * @param delegatee The address to delegate votes to * @param delegationType the type of delegation (VOTING_POWER, PROPOSITION_POWER) * @param nonce The contract state required to match the signature * @param expiry The time at which to expire the signature * @param v The recovery byte of the signature * @param r Half of the ECDSA signature pair * @param s Half of the ECDSA signature pair */ function delegateByTypeBySig( address delegatee, DelegationType delegationType, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) public { bytes32 structHash = keccak256( abi.encode( DELEGATE_BY_TYPE_TYPEHASH, delegatee, uint256(delegationType), nonce, expiry ) ); bytes32 digest = keccak256( abi.encodePacked('\x19\x01', DOMAIN_SEPARATOR, structHash) ); address signatory = ecrecover(digest, v, r, s); require(signatory != address(0), 'INVALID_SIGNATURE'); require(nonce == _nonces[signatory]++, 'INVALID_NONCE'); require(block.timestamp <= expiry, 'INVALID_EXPIRATION'); _delegateByType(signatory, delegatee, delegationType); } /** * @dev Delegates power from signatory to `delegatee` * @param delegatee The address to delegate votes to * @param nonce The contract state required to match the signature * @param expiry The time at which to expire the signature * @param v The recovery byte of the signature * @param r Half of the ECDSA signature pair * @param s Half of the ECDSA signature pair */ function delegateBySig( address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) public { bytes32 structHash = keccak256( abi.encode(DELEGATE_TYPEHASH, delegatee, nonce, expiry) ); bytes32 digest = keccak256( abi.encodePacked('\x19\x01', DOMAIN_SEPARATOR, structHash) ); address signatory = ecrecover(digest, v, r, s); require(signatory != address(0), 'INVALID_SIGNATURE'); require(nonce == _nonces[signatory]++, 'INVALID_NONCE'); require(block.timestamp <= expiry, 'INVALID_EXPIRATION'); _delegateByType(signatory, delegatee, DelegationType.VOTING_POWER); _delegateByType(signatory, delegatee, DelegationType.PROPOSITION_POWER); } /** * @dev Updates the user state related with his accrued rewards * @param user Address of the user * @param userBalance The current balance of the user * @param updateStorage Boolean flag used to update or not the stakerRewardsToClaim of the user * @return The unclaimed rewards that were added to the total accrued */ function _updateCurrentUnclaimedRewards( address user, uint256 userBalance, bool updateStorage ) internal returns (uint256) { uint256 accruedRewards = _updateUserAssetInternal( user, address(this), userBalance, totalSupply() ); uint256 unclaimedRewards = stakerRewardsToClaim[user] + accruedRewards; if (accruedRewards != 0) { if (updateStorage) { stakerRewardsToClaim[user] = unclaimedRewards; } emit RewardsAccrued(user, accruedRewards); } return unclaimedRewards; } /** * @dev returns relevant storage slots for a DelegationType * @param delegationType the requested DelegationType * @return the relevant storage */ function _getDelegationDataByType(DelegationType delegationType) internal view override returns ( mapping(address => mapping(uint256 => Snapshot)) storage, //snapshots mapping(address => uint256) storage, //snapshots count mapping(address => address) storage //delegatees list ) { if (delegationType == DelegationType.VOTING_POWER) { return (_votingSnapshots, _votingSnapshotsCounts, _votingDelegates); } else { return ( _propositionPowerSnapshots, _propositionPowerSnapshotsCounts, _propositionPowerDelegates ); } } }