all files / plugins/assets/convex/vendor/ ConvexStakingWrapper.sol

60.5% Statements 72/119
42.86% Branches 30/70
50% Functions 16/32
60.26% Lines 91/151
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493                                                                                                                                                                                                      122× 122× 122×   122×   122× 122× 122× 122×   122× 122× 122×         122× 122×                       289×                                               182× 182× 182×       122× 122× 122× 122×       122×   122× 122×               122×               122× 122×   122×   122×     122× 122× 62× 62× 62×         62×               62×                   194× 91×       103×           103×                   259×       259×     259× 52×           259×   518× 289× 289×   259× 259× 58× 30×     30× 16×       16× 16×     28×           58×         259× 52×           91×   91× 91× 91× 91×   91×   91×   91× 91× 229×         12× 12× 12×   12×   12×   12× 12× 30×                                                                                                                                                                                                       91×       91× 91× 91× 91×     91×                                                                                                   91×      
// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;
pragma experimental ABIEncoderV2;
 
import "@openzeppelin/contracts-v0.7/math/SafeMath.sol";
import "@openzeppelin/contracts-v0.7/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-v0.7/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts-v0.7/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts-v0.7/utils/ReentrancyGuard.sol";
import "./IRewardStaking.sol";
import "./CvxMining.sol";
 
interface IBooster {
    function poolInfo(uint256 _pid)
        external
        view
        returns (
            address _lptoken,
            address _token,
            address _gauge,
            address _crvRewards,
            address _stash,
            bool _shutdown
        );
}
 
interface IConvexDeposits {
    function deposit(
        uint256 _pid,
        uint256 _amount,
        bool _stake
    ) external returns (bool);
 
    function deposit(
        uint256 _amount,
        bool _lock,
        address _stakeAddress
    ) external;
}
 
// if used as collateral some modifications will be needed to fit the specific platform
 
// Based on audited contracts: https://github.com/convex-eth/platform/blob/main/contracts/contracts/wrappers/CvxCrvStakingWrapper.sol
// TODO check on contract size to see if blocker
contract ConvexStakingWrapper is ERC20, ReentrancyGuard {
    using SafeERC20 for IERC20;
    using SafeMath for uint256;
 
    struct EarnedData {
        address token;
        uint256 amount;
    }
 
    struct RewardType {
        address reward_token;
        address reward_pool;
        uint128 reward_integral;
        uint128 reward_remaining;
        mapping(address => uint256) reward_integral_for;
        mapping(address => uint256) claimable_reward;
    }
 
    //constants/immutables
    address public constant convexBooster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31);
    address public constant crv = address(0xD533a949740bb3306d119CC777fa900bA034cd52);
    address public constant cvx = address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B);
    address public curveToken;
    address public convexToken;
    address public convexPool;
    uint256 public convexPoolId;
    address public collateralVault;
    uint256 private constant CRV_INDEX = 0;
    uint256 private constant CVX_INDEX = 1;
 
    //rewards
    RewardType[] public rewards;
    mapping(address => uint256) public registeredRewards;
 
    //management
    bool public isInit;
    address public owner;
    bool internal _isShutdown;
 
    string internal _tokenname;
    string internal _tokensymbol;
 
    event Deposited(
        address indexed _user,
        address indexed _account,
        uint256 _amount,
        bool _wrapped
    );
    event Withdrawn(address indexed _user, uint256 _amount, bool _unwrapped);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    event RewardsClaimed(IERC20 indexed erc20, uint256 indexed amount);
 
    constructor() public ERC20("StakedConvexToken", "stkCvx") {}
 
    function initialize(uint256 _poolId) external virtual {
        Erequire(!isInit, "already init");
        owner = msg.sender;
        emit OwnershipTransferred(address(0), owner);
 
        (address _lptoken, address _token, , address _rewards, , ) = IBooster(convexBooster)
            .poolInfo(_poolId);
        curveToken = _lptoken;
        convexToken = _token;
        convexPool = _rewards;
        convexPoolId = _poolId;
 
        _tokenname = string(abi.encodePacked("Staked ", ERC20(_token).name()));
        _tokensymbol = string(abi.encodePacked("stk", ERC20(_token).symbol()));
        isInit = true;
 
        // collateralVault = _vault;
 
        //add rewards
        addRewards();
        setApprovals();
    }
 
    function name() public view override returns (string memory) {
        return _tokenname;
    }
 
    function symbol() public view override returns (string memory) {
        return _tokensymbol;
    }
 
    function decimals() public view override returns (uint8) {
        return 18;
    }
 
    modifier onlyOwner() {
        require(owner == msg.sender, "Ownable: caller is not the owner");
        _;
    }
 
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
 
    function renounceOwnership() public virtual onlyOwner {
        emit OwnershipTransferred(owner, address(0));
        owner = address(0);
    }
 
    function shutdown() external onlyOwner {
        _isShutdown = true;
    }
 
    function isShutdown() public view returns (bool) {
        Iif (_isShutdown) return true;
        (, , , , , bool isShutdown_) = IBooster(convexBooster).poolInfo(convexPoolId);
        return isShutdown_;
    }
 
    function setApprovals() public {
        IERC20(curveToken).safeApprove(convexBooster, 0);
        IERC20(curveToken).safeApprove(convexBooster, uint256(-1));
        IERC20(convexToken).safeApprove(convexPool, 0);
        IERC20(convexToken).safeApprove(convexPool, uint256(-1));
    }
 
    function addRewards() public {
        address mainPool = convexPool;
 
        Eif (rewards.length == 0) {
            rewards.push(
                RewardType({
                    reward_token: crv,
                    reward_pool: mainPool,
                    reward_integral: 0,
                    reward_remaining: 0
                })
            );
            rewards.push(
                RewardType({
                    reward_token: cvx,
                    reward_pool: address(0),
                    reward_integral: 0,
                    reward_remaining: 0
                })
            );
            registeredRewards[crv] = CRV_INDEX + 1; //mark registered at index+1
            registeredRewards[cvx] = CVX_INDEX + 1; //mark registered at index+1
            //send to self to warmup state
            IERC20(crv).transfer(address(this), 0);
            //send to self to warmup state
            IERC20(cvx).transfer(address(this), 0);
        }
 
        uint256 extraCount = IRewardStaking(mainPool).extraRewardsLength();
        for (uint256 i = 0; i < extraCount; i++) {
            address extraPool = IRewardStaking(mainPool).extraRewards(i);
            address extraToken = IRewardStaking(extraPool).rewardToken();
            Iif (extraToken == cvx) {
                //update cvx reward pool address
                rewards[CVX_INDEX].reward_pool = extraPool;
            } else Eif (registeredRewards[extraToken] == 0) {
                //add new token to list
                rewards.push(
                    RewardType({
                        reward_token: IRewardStaking(extraPool).rewardToken(),
                        reward_pool: extraPool,
                        reward_integral: 0,
                        reward_remaining: 0
                    })
                );
                registeredRewards[extraToken] = rewards.length; //mark registered at index+1
            }
        }
    }
 
    function rewardLength() external view returns (uint256) {
        return rewards.length;
    }
 
    function _getDepositedBalance(address _account) internal view virtual returns (uint256) {
        if (_account == address(0) || _account == collateralVault) {
            return 0;
        }
        //get balance from collateralVault
 
        return balanceOf(_account);
    }
 
    function _getTotalSupply() internal view virtual returns (uint256) {
        //override and add any supply needed (interest based growth)
 
        return totalSupply();
    }
 
    function _calcRewardIntegral(
        uint256 _index,
        address[2] memory _accounts,
        uint256[2] memory _balances,
        uint256 _supply,
        bool _isClaim
    ) internal {
        RewardType storage reward = rewards[_index];
 
        //get difference in balance and remaining rewards
        //getReward is unguarded so we use reward_remaining to keep track of how much was actually claimed
        uint256 bal = IERC20(reward.reward_token).balanceOf(address(this));
        // uint256 d_reward = bal.sub(reward.reward_remaining);
 
        if (_supply > 0 && bal.sub(reward.reward_remaining) > 0) {
            reward.reward_integral =
                reward.reward_integral +
                uint128(bal.sub(reward.reward_remaining).mul(1e20).div(_supply));
        }
 
        //update user integrals
        for (uint256 u = 0; u < _accounts.length; u++) {
            //do not give rewards to address 0
            if (_accounts[u] == address(0)) continue;
            Iif (_accounts[u] == collateralVault) continue;
            if (_isClaim && u != 0) continue; //only update/claim for first address and use second as forwarding
 
            uint256 userI = reward.reward_integral_for[_accounts[u]];
            if (_isClaim || userI < reward.reward_integral) {
                if (_isClaim) {
                    uint256 receiveable = reward.claimable_reward[_accounts[u]].add(
                        _balances[u].mul(uint256(reward.reward_integral).sub(userI)).div(1e20)
                    );
                    if (receiveable > 0) {
                        reward.claimable_reward[_accounts[u]] = 0;
                        //cheat for gas savings by transfering to the second index in accounts list
                        //if claiming only the 0 index will update so 1 index can hold forwarding info
                        //guaranteed to have an address in u+1 so no need to check
                        IERC20(reward.reward_token).safeTransfer(_accounts[u + 1], receiveable);
                        bal = bal.sub(receiveable);
                    }
                } else {
                    reward.claimable_reward[_accounts[u]] = reward
                        .claimable_reward[_accounts[u]]
                        .add(
                            _balances[u].mul(uint256(reward.reward_integral).sub(userI)).div(1e20)
                        );
                }
                reward.reward_integral_for[_accounts[u]] = reward.reward_integral;
            }
        }
 
        //update remaining reward here since balance could have changed if claiming
        if (bal != reward.reward_remaining) {
            reward.reward_remaining = uint128(bal);
        }
    }
 
    function _checkpoint(address[2] memory _accounts) internal EnonReentrant {
        //if shutdown, no longer checkpoint in case there are problems
        Iif (isShutdown()) return;
 
        uint256 supply = _getTotalSupply();
        uint256[2] memory depositedBalance;
        depositedBalance[0] = _getDepositedBalance(_accounts[0]);
        depositedBalance[1] = _getDepositedBalance(_accounts[1]);
 
        IRewardStaking(convexPool).getReward(address(this), true);
 
        _claimExtras();
 
        uint256 rewardCount = rewards.length;
        for (uint256 i = 0; i < rewardCount; i++) {
            _calcRewardIntegral(i, _accounts, depositedBalance, supply, false);
        }
    }
 
    function _checkpointAndClaim(address[2] memory _accounts) internal EnonReentrant {
        uint256 supply = _getTotalSupply();
        uint256[2] memory depositedBalance;
        depositedBalance[0] = _getDepositedBalance(_accounts[0]); //only do first slot
 
        IRewardStaking(convexPool).getReward(address(this), true);
 
        _claimExtras();
 
        uint256 rewardCount = rewards.length;
        for (uint256 i = 0; i < rewardCount; i++) {
            _calcRewardIntegral(i, _accounts, depositedBalance, supply, true);
        }
    }
 
    //claim any rewards not part of the convex pool
    function _claimExtras() internal virtual {
        //override and add external reward claiming
    }
 
    function user_checkpoint(address _account) external returns (bool) {
        _checkpoint([_account, address(0)]);
        return true;
    }
 
    function totalBalanceOf(address _account) external view returns (uint256) {
        return _getDepositedBalance(_account);
    }
 
    //run earned as a mutable function to claim everything before calculating earned rewards
    function earned(address _account) external returns (EarnedData[] memory claimable) {
        IRewardStaking(convexPool).getReward(address(this), true);
        _claimExtras();
        return _earned(_account);
    }
 
    //run earned as a non-mutative function that may not claim everything, but should report standard convex rewards
    function earnedView(address _account) external view returns (EarnedData[] memory claimable) {
        return _earned(_account);
    }
 
    function _earned(address _account) internal view returns (EarnedData[] memory claimable) {
        uint256 supply = _getTotalSupply();
        // uint256 depositedBalance = _getDepositedBalance(_account);
        uint256 rewardCount = rewards.length;
        claimable = new EarnedData[](rewardCount);
 
        for (uint256 i = 0; i < rewardCount; i++) {
            RewardType storage reward = rewards[i];
 
            //change in reward is current balance - remaining reward + earned
            uint256 bal = IERC20(reward.reward_token).balanceOf(address(this));
            uint256 d_reward = bal.sub(reward.reward_remaining);
 
            //some rewards (like minted cvx) may not have a reward pool directly on the convex pool so check if it exists
            if (reward.reward_pool != address(0)) {
                //add earned from the convex reward pool for the given token
                d_reward = d_reward.add(IRewardStaking(reward.reward_pool).earned(address(this)));
            }
 
            uint256 I = reward.reward_integral;
            if (supply > 0) {
                I = I + d_reward.mul(1e20).div(supply);
            }
 
            uint256 newlyClaimable = _getDepositedBalance(_account)
                .mul(I.sub(reward.reward_integral_for[_account]))
                .div(1e20);
            claimable[i].amount = claimable[i].amount.add(
                reward.claimable_reward[_account].add(newlyClaimable)
            );
            claimable[i].token = reward.reward_token;
 
            //calc cvx minted from crv and add to cvx claimables
            //note: crv is always index 0 so will always run before cvx
            if (i == CRV_INDEX) {
                //because someone can call claim for the pool outside of checkpoints, need to recalculate crv without the local balance
                I = reward.reward_integral;
                if (supply > 0) {
                    I =
                        I +
                        IRewardStaking(reward.reward_pool).earned(address(this)).mul(1e20).div(
                            supply
                        );
                }
                newlyClaimable = _getDepositedBalance(_account)
                    .mul(I.sub(reward.reward_integral_for[_account]))
                    .div(1e20);
                claimable[CVX_INDEX].amount = CvxMining.ConvertCrvToCvx(newlyClaimable);
                claimable[CVX_INDEX].token = cvx;
            }
        }
        return claimable;
    }
 
    function claimRewards() external {
        uint256 cvxOldBal = IERC20(cvx).balanceOf(msg.sender);
        uint256 crvOldBal = IERC20(crv).balanceOf(msg.sender);
        _checkpointAndClaim([address(msg.sender), address(msg.sender)]);
        emit RewardsClaimed(IERC20(cvx), IERC20(cvx).balanceOf(msg.sender) - cvxOldBal);
        emit RewardsClaimed(IERC20(crv), IERC20(crv).balanceOf(msg.sender) - crvOldBal);
    }
 
    function getReward(address _account) external {
        //claim directly in checkpoint logic to save a bit of gas
        _checkpointAndClaim([_account, _account]);
    }
 
    function getReward(address _account, address _forwardTo) external {
        require(msg.sender == _account, "!self");
        //claim directly in checkpoint logic to save a bit of gas
        //pack forwardTo into account array to save gas so that a proxy etc doesnt have to double transfer
        _checkpointAndClaim([_account, _forwardTo]);
    }
 
    //deposit a curve token
    function deposit(uint256 _amount, address _to) external {
        Erequire(!isShutdown(), "shutdown");
 
        //dont need to call checkpoint since _mint() will
 
        Eif (_amount > 0) {
            _mint(_to, _amount);
            IERC20(curveToken).safeTransferFrom(msg.sender, address(this), _amount);
            IConvexDeposits(convexBooster).deposit(convexPoolId, _amount, true);
        }
 
        emit Deposited(msg.sender, _to, _amount, true);
    }
 
    //stake a convex token
    function stake(uint256 _amount, address _to) external {
        require(!isShutdown(), "shutdown");
 
        //dont need to call checkpoint since _mint() will
 
        if (_amount > 0) {
            _mint(_to, _amount);
            IERC20(convexToken).safeTransferFrom(msg.sender, address(this), _amount);
            IRewardStaking(convexPool).stake(_amount);
        }
 
        emit Deposited(msg.sender, _to, _amount, false);
    }
 
    //withdraw to convex deposit token
    function withdraw(uint256 _amount) external {
        //dont need to call checkpoint since _burn() will
 
        if (_amount > 0) {
            _burn(msg.sender, _amount);
            IRewardStaking(convexPool).withdraw(_amount, false);
            IERC20(convexToken).safeTransfer(msg.sender, _amount);
        }
 
        emit Withdrawn(msg.sender, _amount, false);
    }
 
    //withdraw to underlying curve lp token
    function withdrawAndUnwrap(uint256 _amount) external {
        //dont need to call checkpoint since _burn() will
 
        if (_amount > 0) {
            _burn(msg.sender, _amount);
            IRewardStaking(convexPool).withdrawAndUnwrap(_amount, false);
            IERC20(curveToken).safeTransfer(msg.sender, _amount);
        }
 
        //events
        emit Withdrawn(msg.sender, _amount, true);
    }
 
    function _beforeTokenTransfer(
        address _from,
        address _to,
        uint256
    ) internal override {
        _checkpoint([_from, _to]);
    }
}