diff --git a/angle/contracts/BaseStrategy.sol b/angle/contracts/BaseStrategy.sol new file mode 100644 index 0000000..94e71be --- /dev/null +++ b/angle/contracts/BaseStrategy.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "./interfaces/IController.sol"; + +abstract contract BaseStrategy { + using SafeERC20 for IERC20; + using Address for address; + + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public governance; + address public controller; + address public strategist; + address public want; + + uint256 public earned; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + constructor(address _controller, address _want) { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + want = _want; + } + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + modifier onlyAdmin() { + require(msg.sender == controller || msg.sender == strategist, "!admin"); + _; + } + + function clean(IERC20 _asset) external onlyGovernance returns (uint256 balance) { + require(want != address(_asset), "want"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(governance, balance); + } + + function withdraw(uint256 _amount) external virtual onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + + if (_balance < _amount) { + _withdrawSome(_amount - _balance); + } + + uint256 _fee = _amount * withdrawalFee / FEE_DENOMINATOR; + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); + IERC20(want).safeTransfer(_vault, _amount - _fee); + } + + function withdrawAll() external virtual onlyController returns (uint256 balance) { + _withdrawSome(balanceOfPool()); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); + IERC20(want).safeTransfer(_vault, balance); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant() + balanceOfPool(); + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function setStrategist(address _strategist) external onlyGovernance { + strategist = _strategist; + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } + + /* Implemented by strategy */ + + function name() external pure virtual returns (string memory); + + function balanceOfPool() public view virtual returns (uint256); + + function deposit() public virtual; + + function _withdrawSome(uint256 _amount) internal virtual; +} diff --git a/angle/contracts/Controller.sol b/angle/contracts/Controller.sol new file mode 100644 index 0000000..8f7de35 --- /dev/null +++ b/angle/contracts/Controller.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "./interfaces/IStrategy.sol"; + +contract Controller { + using SafeERC20 for IERC20; + using Address for address; + //using SafeMath for uint256; + + address public governance; + address public strategist; + + address public rewards; + mapping(address => address) public vaults; + mapping(address => address) public strategies; + + mapping(address => mapping(address => bool)) public approvedStrategies; + + uint256 public constant max = 10000; + + event SetStrategy(address indexed asset, address indexed strategy); + event ApproveStrategy(address indexed asset, address indexed strategy); + event SetVault(address indexed asset, address indexed vault); + + constructor(address _rewards) { + governance = msg.sender; + strategist = msg.sender; + rewards = _rewards; + } + + modifier onlyGovernance() { + require(msg.sender == governance, "!gov"); + _; + } + + modifier onlyAdmin() { + require(msg.sender == governance || msg.sender == strategist, "!(gov||strategist)"); + _; + } + + function setRewards(address _rewards) public onlyGovernance { + rewards = _rewards; + } + + function setStrategist(address _strategist) public onlyGovernance { + strategist = _strategist; + } + + function setGovernance(address _governance) public onlyGovernance { + governance = _governance; + } + + function setVault(address _token, address _vault) public { + require(msg.sender == strategist || msg.sender == governance, "!strategist"); + require(vaults[_token] == address(0), "vault"); + vaults[_token] = _vault; + emit SetVault(_token, _vault); + } + + function approveStrategy(address _token, address _strategy) public onlyGovernance { + approvedStrategies[_token][_strategy] = true; + emit ApproveStrategy(_token, _strategy); + } + + function revokeStrategy(address _token, address _strategy) public onlyGovernance { + approvedStrategies[_token][_strategy] = false; + } + + function setStrategy(address _token, address _strategy) public onlyAdmin { + require(approvedStrategies[_token][_strategy] == true, "!approved"); + + address _current = strategies[_token]; + if (_current != address(0)) { + IStrategy(_current).withdrawAll(); + } + strategies[_token] = _strategy; + emit SetStrategy(_token, _strategy); + } + + function earn(address _token, uint256 _amount) public { + address _strategy = strategies[_token]; + IERC20(_token).safeTransfer(_strategy, _amount); + IStrategy(_strategy).deposit(); + } + + function balanceOf(address _token) external view returns (uint256) { + return IStrategy(strategies[_token]).balanceOf(); + } + + function withdrawAll(address _token) public onlyAdmin { + IStrategy(strategies[_token]).withdrawAll(); + } + + function inCaseTokensGetStuck(address _token, uint256 _amount) public onlyAdmin { + IERC20(_token).safeTransfer(msg.sender, _amount); + } + + function inCaseStrategyTokenGetStuck(address _strategy, address _token) public onlyAdmin { + IStrategy(_strategy).withdraw(_token); + } + + function withdraw(address _token, uint256 _amount) public { + require(msg.sender == vaults[_token], "!vault"); + IStrategy(strategies[_token]).withdraw(_amount); + } +} diff --git a/angle/contracts/GaugeMultiRewards.sol b/angle/contracts/GaugeMultiRewards.sol new file mode 100644 index 0000000..39cb07d --- /dev/null +++ b/angle/contracts/GaugeMultiRewards.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/security/Pausable.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract GaugeMultiRewards is ReentrancyGuard, Pausable { + using SafeERC20 for IERC20; + + /* ========== STATE VARIABLES ========== */ + + struct Reward { + address rewardsDistributor; + uint256 rewardsDuration; + uint256 periodFinish; + uint256 rewardRate; + uint256 lastUpdateTime; + uint256 rewardPerTokenStored; + } + + IERC20 public stakingToken; + + mapping(address => Reward) public rewardData; + + address public governance; + address[] public rewardTokens; + + // user -> reward token -> amount + mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid; + mapping(address => mapping(address => uint256)) public rewards; + + uint256 private _totalSupply; + uint256 public derivedSupply; + + mapping(address => uint256) private _balances; + mapping(address => uint256) public derivedBalances; + + /* ========== CONSTRUCTOR ========== */ + + constructor( + address _stakingToken + ) { + governance = msg.sender; + stakingToken = IERC20(_stakingToken); + } + + function addReward( + address _rewardsToken, + address _rewardsDistributor, + uint256 _rewardsDuration + ) public onlyGovernance { + require(rewardData[_rewardsToken].rewardsDuration == 0); + rewardTokens.push(_rewardsToken); + rewardData[_rewardsToken].rewardsDistributor = _rewardsDistributor; + rewardData[_rewardsToken].rewardsDuration = _rewardsDuration; + } + + /* ========== VIEWS ========== */ + + function totalSupply() external view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) external view returns (uint256) { + return _balances[account]; + } + + function lastTimeRewardApplicable(address _rewardsToken) public view returns (uint256) { + return Math.min(block.timestamp, rewardData[_rewardsToken].periodFinish); + } + + function rewardPerToken(address _rewardsToken) public view returns (uint256) { + if (_totalSupply == 0) { + return rewardData[_rewardsToken].rewardPerTokenStored; + } + return rewardData[_rewardsToken].rewardPerTokenStored + + (((lastTimeRewardApplicable(_rewardsToken) + - rewardData[_rewardsToken].lastUpdateTime) + * rewardData[_rewardsToken].rewardRate + * 1e6) // from 1e18 + / _totalSupply + ); + } + + function earned(address _account, address _rewardsToken) public view returns (uint256) { + uint256 userBalance = _balances[_account]; + + return + userBalance * (rewardPerToken(_rewardsToken) - userRewardPerTokenPaid[_account][_rewardsToken]) / 1e6 + rewards[_account][_rewardsToken]; + } + + function getRewardForDuration(address _rewardsToken) external view returns (uint256) { + return rewardData[_rewardsToken].rewardRate * rewardData[_rewardsToken].rewardsDuration; + } + + /* ========== MUTATIVE FUNCTIONS ========== */ + + function setRewardsDistributor(address _rewardsToken, address _rewardsDistributor) external onlyGovernance { + rewardData[_rewardsToken].rewardsDistributor = _rewardsDistributor; + } + + function _stake(uint256 amount, address account) internal nonReentrant whenNotPaused updateReward(account) { + require(amount > 0, "Cannot stake 0"); + _totalSupply = _totalSupply + amount; + _balances[account] = _balances[account] + amount; + stakingToken.safeTransferFrom(msg.sender, address(this), amount); + emit Staked(account, amount); + } + + function _withdraw(uint256 amount, address account) internal nonReentrant updateReward(account) { + require(amount > 0, "Cannot withdraw 0"); + _totalSupply = _totalSupply - amount; + _balances[account] = _balances[account] - amount; + stakingToken.safeTransfer(msg.sender, amount); + emit Withdrawn(account, amount); + } + + function stake(uint256 amount) external { + _stake(amount, msg.sender); + } + + function stakeFor(address account, uint256 amount) external { + _stake(amount, account); + } + + function withdraw(uint256 amount) external { + _withdraw(amount, msg.sender); + } + + function withdrawFor(address account, uint256 amount) external { + require(tx.origin == account, "withdrawFor: account != tx.origin"); + _withdraw(amount, account); + } + + function getRewardFor(address account) public nonReentrant updateReward(account) { + for (uint256 i; i < rewardTokens.length; i++) { + address _rewardsToken = rewardTokens[i]; + uint256 reward = rewards[account][_rewardsToken]; + if (reward > 0) { + rewards[account][_rewardsToken] = 0; + IERC20(_rewardsToken).safeTransfer(account, reward); + emit RewardPaid(account, _rewardsToken, reward); + } + } + } + + /* ========== RESTRICTED FUNCTIONS ========== */ + + function setGovernance(address _governance) public onlyGovernance { + governance = _governance; + } + + function notifyRewardAmount(address _rewardsToken, uint256 reward) external updateReward(address(0)) { + require(rewardData[_rewardsToken].rewardsDistributor == msg.sender); + // handle the transfer of reward tokens via `transferFrom` to reduce the number + // of transactions required and ensure correctness of the reward amount + IERC20(_rewardsToken).safeTransferFrom(msg.sender, address(this), reward); + + if (block.timestamp >= rewardData[_rewardsToken].periodFinish) { + rewardData[_rewardsToken].rewardRate = reward / rewardData[_rewardsToken].rewardsDuration; + } else { + uint256 remaining = rewardData[_rewardsToken].periodFinish - block.timestamp; + uint256 leftover = remaining * rewardData[_rewardsToken].rewardRate; + rewardData[_rewardsToken].rewardRate = (reward + leftover) / rewardData[_rewardsToken].rewardsDuration; + } + + rewardData[_rewardsToken].lastUpdateTime = block.timestamp; + rewardData[_rewardsToken].periodFinish = block.timestamp + rewardData[_rewardsToken].rewardsDuration; + emit RewardAdded(reward); + } + + function recoverERC20( + address tokenAddress, + uint256 tokenAmount, + address destination + ) external onlyGovernance { + require(tokenAddress != address(stakingToken), "Cannot withdraw staking token"); + require(rewardData[tokenAddress].lastUpdateTime == 0, "Cannot withdraw reward token"); + IERC20(tokenAddress).safeTransfer(destination, tokenAmount); + emit Recovered(tokenAddress, tokenAmount); + } + + function setRewardsDuration(address _rewardsToken, uint256 _rewardsDuration) external { + require(block.timestamp > rewardData[_rewardsToken].periodFinish, "Reward period still active"); + require(rewardData[_rewardsToken].rewardsDistributor == msg.sender); + require(_rewardsDuration > 0, "Reward duration must be non-zero"); + rewardData[_rewardsToken].rewardsDuration = _rewardsDuration; + emit RewardsDurationUpdated(_rewardsToken, rewardData[_rewardsToken].rewardsDuration); + } + + /* ========== MODIFIERS ========== */ + + modifier updateReward(address account) { + for (uint256 i; i < rewardTokens.length; i++) { + address token = rewardTokens[i]; + rewardData[token].rewardPerTokenStored = rewardPerToken(token); + rewardData[token].lastUpdateTime = lastTimeRewardApplicable(token); + if (account != address(0)) { + rewards[account][token] = earned(account, token); + userRewardPerTokenPaid[account][token] = rewardData[token].rewardPerTokenStored; + } + } + _; + } + + modifier onlyGovernance() { + require(msg.sender == governance, "!gov"); + _; + } + + /* ========== EVENTS ========== */ + + event RewardAdded(uint256 reward); + event Staked(address indexed user, uint256 amount); + event Withdrawn(address indexed user, uint256 amount); + event RewardPaid(address indexed user, address indexed rewardsToken, uint256 reward); + event RewardsDurationUpdated(address token, uint256 newDuration); + event Recovered(address token, uint256 amount); +} \ No newline at end of file diff --git a/angle/contracts/VaultAngle.sol b/angle/contracts/VaultAngle.sol new file mode 100644 index 0000000..e0145ed --- /dev/null +++ b/angle/contracts/VaultAngle.sol @@ -0,0 +1,145 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "hardhat/console.sol"; +import "./interfaces/IMultiRewards.sol"; +import "./interfaces/IAngle.sol"; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IStrategy { + function stake() external; +} + +contract Vault is ERC20 { + using SafeERC20 for IERC20; + using Address for address; + + IERC20 public token; + + address public sanUSDC_EUR = 0x9C215206Da4bf108aE5aEEf9dA7caD3352A36Dad; + address public staking = 0x2Fa1255383364F6e17Be6A6aC7A56C9aCD6850a3; + address public stableMaster = 0x5adDc89785D75C86aB939E9e15bfBBb7Fc086A87; + address public poolManager = 0xe9f183FC656656f1F17af1F2b0dF79b8fF9ad8eD; + address public governance; + address public controller; + address public gauge; + + constructor(address _token, address _controller) ERC20("Stake DAO Angle USDC strat", "sdsanUSDC_EUR") { + token = IERC20(_token); + governance = msg.sender; + controller = _controller; + } + + function deposit(uint256 _amount) public { + require(gauge != address(0), "Gauge not yet initialized"); + token.safeTransferFrom(msg.sender, address(this), _amount); + // rate 1:1 with san LP minted + uint256 shares = _earn(); + _mint(address(this), shares); + IERC20(address(this)).approve(gauge, shares); + IMultiRewards(gauge).stakeFor(msg.sender, shares); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function _earn() internal returns (uint256) { + uint256 _bal = token.balanceOf(address(this)); + uint256 stakedBefore = IERC20(sanUSDC_EUR).balanceOf(address(this)); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + uint256 stakedAfter = IERC20(sanUSDC_EUR).balanceOf(address(this)); + return stakedAfter - stakedBefore; + } + + function withdraw(uint256 _shares) public { + uint256 userTotalShares = IMultiRewards(gauge).balanceOf(msg.sender); + require(_shares <= userTotalShares, "Not enough staked"); + IMultiRewards(gauge).withdrawFor(msg.sender, _shares); + _burn(address(this), _shares); + uint256 sanUsdcEurBal = IERC20(sanUSDC_EUR).balanceOf(address(this)); + if (_shares > sanUsdcEurBal) { + IController(controller).withdraw(address(token), _shares); + } else { + IStableMaster(stableMaster).withdraw(_shares, address(this), address(this), IPoolManager(poolManager)); + } + uint256 usdcAmount = IERC20(token).balanceOf(address(this)); + + token.safeTransfer(msg.sender, usdcAmount); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + // Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + function harvest(address reserve, uint256 amount) external { + require(msg.sender == controller, "!controller"); + require(reserve != address(token), "token"); + IERC20(reserve).safeTransfer(controller, amount); + } + + function getPricePerFullShare() public pure returns (uint256) { + return 1e6; + } + + function balance() public view returns (uint256) { + return IController(controller).balanceOf(address(token)); + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) public { + require(msg.sender == governance, "!governance"); + controller = _controller; + } + + function setGauge(address _gauge) public { + require(msg.sender == governance, "!governance"); + gauge = _gauge; + } + + function decimals() public view override returns (uint8) { + return 6; + } + + function earn() external { + require(msg.sender == governance, "!governance"); + address strategy = IController(controller).strategies(address(token)); + uint256 _bal = IERC20(sanUSDC_EUR).balanceOf(address(this)); + IERC20(sanUSDC_EUR).safeTransfer(strategy, _bal); + IStrategy(strategy).stake(); + } + + function withdrawRescue() external { + uint256 userTotalShares = IMultiRewards(gauge).balanceOf(msg.sender); + IMultiRewards(gauge).withdrawFor(msg.sender, userTotalShares); + _burn(address(this), userTotalShares); + IERC20(sanUSDC_EUR).transfer(msg.sender, userTotalShares); + } +} diff --git a/angle/contracts/interfaces/IAngle.sol b/angle/contracts/interfaces/IAngle.sol new file mode 100644 index 0000000..789d694 --- /dev/null +++ b/angle/contracts/interfaces/IAngle.sol @@ -0,0 +1,544 @@ +// SPDX-License-Identifier: GNU GPLv3 +pragma solidity >=0.8.2; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// ====================================== IAngle.sol =============================== +// This file contains the interfaces for the main contracts of Angle protocol. +// Some of these contracts need to be deployed several times across the protocol with different +// initializations. We only leave in the following interfaces the user-facing functions +// that anyone can call without having a role. There are some view functions and some +// state-changing functions. + +/// @notice Interface for the `PoolManager` contract handling the collateral of the protocol +/// @dev There is one such contract per stablecoin/collateral pair +interface IPoolManager { + /// @return apr Estimated Annual Percentage Rate for SLPs based on lending to other protocols + function estimatedAPR() external view returns (uint256 apr); + + /// @return The amount of the underlying collateral that the contract currently owns + function getBalance() external view returns (uint256); + + /// @return The amount of collateral owned by this contract plus the amount that has been lent to strategies + function getTotalAsset() external view returns (uint256); +} + +/// @notice Interface for `StableMaster`, the contract handling all the collateral types accepted for a given stablecoin +interface IStableMaster { + // Struct to handle all the parameters to manage the fees + // related to a given collateral pool (associated to the stablecoin) + struct MintBurnData { + // Values of the thresholds to compute the minting fees + // depending on HA hedge (scaled by `BASE_PARAMS`) + uint64[] xFeeMint; + // Values of the fees at thresholds (scaled by `BASE_PARAMS`) + uint64[] yFeeMint; + // Values of the thresholds to compute the burning fees + // depending on HA hedge (scaled by `BASE_PARAMS`) + uint64[] xFeeBurn; + // Values of the fees at thresholds (scaled by `BASE_PARAMS`) + uint64[] yFeeBurn; + // Max proportion of collateral from users that can be covered by HAs + // It is exactly the same as the parameter of the same name in `PerpetualManager`, whenever one is updated + // the other changes accordingly + uint64 targetHAHedge; + // Minting fees correction set by the `FeeManager` contract: they are going to be multiplied + // to the value of the fees computed using the hedge curve + // Scaled by `BASE_PARAMS` + uint64 bonusMalusMint; + // Burning fees correction set by the `FeeManager` contract: they are going to be multiplied + // to the value of the fees computed using the hedge curve + // Scaled by `BASE_PARAMS` + uint64 bonusMalusBurn; + // Parameter used to limit the number of stablecoins that can be issued using the concerned collateral + uint256 capOnStableMinted; + } + + // Struct to handle all the variables and parameters to handle SLPs in the protocol + // including the fraction of interests they receive or the fees to be distributed to + // them + struct SLPData { + // Last timestamp at which the `sanRate` has been updated for SLPs + uint256 lastBlockUpdated; + // Fees accumulated from previous blocks and to be distributed to SLPs + uint256 lockedInterests; + // Max interests used to update the `sanRate` in a single block + // Should be in collateral token base + uint256 maxInterestsDistributed; + // Amount of fees left aside for SLPs and that will be distributed + // when the protocol is collateralized back again + uint256 feesAside; + // Part of the fees normally going to SLPs that is left aside + // before the protocol is collateralized back again (depends on collateral ratio) + // Updated by keepers and scaled by `BASE_PARAMS` + uint64 slippageFee; + // Portion of the fees from users minting and burning + // that goes to SLPs (the rest goes to surplus) + uint64 feesForSLPs; + // Slippage factor that's applied to SLPs exiting (depends on collateral ratio) + // If `slippage = BASE_PARAMS`, SLPs can get nothing, if `slippage = 0` they get their full claim + // Updated by keepers and scaled by `BASE_PARAMS` + uint64 slippage; + // Portion of the interests from lending + // that goes to SLPs (the rest goes to surplus) + uint64 interestsForSLPs; + } + struct Collateral { + // Interface for the token accepted by the underlying `PoolManager` contract + IERC20 token; + // Reference to the `SanToken` for the pool + ISanToken sanToken; + // Reference to the `PerpetualManager` for the pool + IPerpetualManager perpetualManager; + // Adress of the oracle for the change rate between + // collateral and the corresponding stablecoin + IOracle oracle; + // Amount of collateral in the reserves that comes from users + // converted in stablecoin value. Updated at minting and burning. + // A `stocksUsers` of 10 for a collateral type means that overall the balance of the collateral from users + // that minted/burnt stablecoins using this collateral is worth 10 of stablecoins + uint256 stocksUsers; + // Exchange rate between sanToken and collateral + uint256 sanRate; + // Base used in the collateral implementation (ERC20 decimal) + uint256 collatBase; + // Parameters for SLPs and update of the `sanRate` + SLPData slpData; + // All the fees parameters + MintBurnData feeData; + } + + /// @notice Lets a user send collateral to the system to mint stablecoins + /// @param amount Amount of collateral sent + /// @param user Address of the contract or the person to give the minted tokens to + /// @param poolManager Address of the `PoolManager` of the required collateral + /// @param minStableAmount Minimum amount of stablecoins the user wants to get with this transaction + /// @dev The `poolManager` refers to the collateral that the user wants to send + /// @dev `minStableAmount` serves as a slippage protection for users + function mint( + uint256 amount, + address user, + IPoolManager poolManager, + uint256 minStableAmount + ) external; + + /// @notice Lets a user burn agTokens (stablecoins) and receive the collateral specified by the `poolManager` + /// in exchange + /// @param amount Amount of stable asset burnt + /// @param burner Address from which the agTokens will be burnt + /// @param dest Address where collateral is going to be sent + /// @param poolManager Collateral type requested by the user burning + /// @param minCollatAmount Minimum amount of collateral that the user wants to get with this transaction + function burn( + uint256 amount, + address burner, + address dest, + IPoolManager poolManager, + uint256 minCollatAmount + ) external; + + /// @notice Lets a SLP enter the protocol by sending collateral to the system in exchange of sanTokens + /// @param user Address of the SLP to send sanTokens to + /// @param amount Amount of collateral sent + /// @param poolManager Address of the `PoolManager` of the required collateral: the corresponding collateral + /// type is the one that is going to be sent by the user + function deposit( + uint256 amount, + address user, + IPoolManager poolManager + ) external; + + /// @notice Lets a SLP burn of sanTokens and receive the corresponding collateral back in exchange at the + /// current exchange rate between sanTokens and collateral + /// @param amount Amount of sanTokens burnt by the SLP + /// @param burner Address that will burn its sanTokens + /// @param dest Address that will receive the collateral + /// @param poolManager Address of the `PoolManager` of the required collateral + function withdraw( + uint256 amount, + address burner, + address dest, + IPoolManager poolManager + ) external; + + /// @return Collateral ratio for this stablecoin + function getCollateralRatio() external view returns (uint256); + + function collateralMap(IPoolManager poolManager) + external + view + returns ( + IERC20 token, + ISanToken sanToken, + IPerpetualManager perpetualManager, + IOracle oracle, + uint256 stocksUsers, + uint256 sanRate, + uint256 collatBase, + SLPData memory slpData, + MintBurnData memory feeData + ); +} + +/// @notice Interface for the contract managing perpetuals: there is one such contract per collateral/stablecoin +/// pair in the protocol +interface IPerpetualManager { + /// @notice Lets a HA join the protocol and create a perpetual + /// @param owner Address of the future owner of the perpetual + /// @param margin Amount of collateral brought by the HA + /// @param committedAmount Amount of collateral hedged by the HA + /// @param maxOracleRate Maximum oracle value that the HA wants to see stored in the perpetual + /// @param minNetMargin Minimum net margin that the HA is willing to see stored in the perpetual + /// @return perpetualID The ID of the perpetual opened by this HA + /// @dev The future owner of the perpetual cannot be the zero address + /// @dev It is possible to open a perpetual on behalf of someone else + /// @dev The `maxOracleRate` parameter serves as a protection against oracle manipulations for HAs opening perpetuals + /// @dev `minNetMargin` is a protection against too big variations in the fees for HAs + function openPerpetual( + address owner, + uint256 margin, + uint256 committedAmount, + uint256 maxOracleRate, + uint256 minNetMargin + ) external returns (uint256 perpetualID); + + /// @notice Lets a HA close a perpetual owned or controlled for the stablecoin/collateral pair associated + /// to this `PerpetualManager` contract + /// @param perpetualID ID of the perpetual to close + /// @param to Address which will receive the proceeds from this perpetual + /// @param minCashOutAmount Minimum net cash out amount that the HA is willing to get for closing the + /// perpetual + /// @dev The HA gets the current amount of her position depending on the entry oracle value + /// and current oracle value minus some transaction fees computed on the committed amount + /// @dev `msg.sender` should be the owner of `perpetualID` or be approved for this perpetual + /// @dev If the `PoolManager` does not have enough collateral, the perpetual owner will be converted to a SLP and + /// receive sanTokens + /// @dev The `minCashOutAmount` serves as a protection for HAs closing their perpetuals: it protects them both + /// from fees that would have become too high and from a too big decrease in oracle value + function closePerpetual( + uint256 perpetualID, + address to, + uint256 minCashOutAmount + ) external; + + /// @notice Lets a HA increase the `margin` in a perpetual she controls for this + /// stablecoin/collateral pair + /// @param perpetualID ID of the perpetual to which amount should be added to `margin` + /// @param amount Amount to add to the perpetual's `margin` + function addToPerpetual(uint256 perpetualID, uint256 amount) external; + + /// @notice Lets a HA decrease the `margin` in a perpetual she controls for this + /// stablecoin/collateral pair + /// @param perpetualID ID of the perpetual from which collateral should be removed + /// @param amount Amount to remove from the perpetual's `margin` + /// @param to Address which will receive the collateral removed from this perpetual + function removeFromPerpetual( + uint256 perpetualID, + uint256 amount, + address to + ) external; + + // =========================== External View Function ========================== + + /// @notice Returns the `cashOutAmount` of the perpetual owned by someone at a given oracle value + /// @param perpetualID ID of the perpetual + /// @param rate Oracle value + /// @return The `cashOutAmount` of the perpetual + /// @return Whether the position of the perpetual is now too small compared with its initial position + function getCashOutAmount(uint256 perpetualID, uint256 rate) external view returns (uint256, uint256); + + // =========================== Reward Distribution ============================= + + /// @notice Allows to check the amount of reward tokens earned by a perpetual + /// @param perpetualID ID of the perpetual to check + /// @return The earned tokens by the perpetual that have not been claimed yet + function earned(uint256 perpetualID) external view returns (uint256); + + /// @notice Allows a perpetual owner to withdraw rewards + /// @param perpetualID ID of the perpetual which accumulated tokens + /// @dev Only an approved caller can claim the rewards for the perpetual with perpetualID + function getReward(uint256 perpetualID) external; + + // =============================== ERC721 logic ================================ + + /// @notice Gets the balance of an owner + /// @param owner Address of the owner + /// @return Balance (ie the number of perpetuals) owned by a HA + function balanceOf(address owner) external view returns (uint256); + + /// @notice Gets the owner of the perpetual with ID perpetualID + /// @param perpetualID ID of the perpetual + /// @return Owner address + function ownerOf(uint256 perpetualID) external view returns (address); + + /// @notice Approves to an address specified by `to` a perpetual specified by `perpetualID` + /// @param to Address to approve the perpetual to + /// @param perpetualID ID of the perpetual + function approve(address to, uint256 perpetualID) external; + + /// @param perpetualID ID of the concerned perpetual + /// @return Approved address by a perpetual owner + function getApproved(uint256 perpetualID) external view returns (address); + + /// @notice Sets approval on all perpetuals owned by the owner to an operator + /// @param operator Address to approve (or block) on all perpetuals + /// @param approved Whether the sender wants to approve or block the operator + function setApprovalForAll(address operator, bool approved) external; + + /// @param owner Owner of perpetuals + /// @param operator Address to check if approved + /// @return If the operator address is approved on all perpetuals by the owner + function isApprovedForAll(address owner, address operator) external view returns (bool); + + /// @param perpetualID ID of the perpetual + /// @return If the sender address is approved for the perpetualId + function isApprovedOrOwner(address spender, uint256 perpetualID) external view returns (bool); + + /// @notice Transfers the `perpetualID` from an address to another + /// @param from Source address + /// @param to Destination a address + /// @param perpetualID ID of the perpetual to transfer + function transferFrom( + address from, + address to, + uint256 perpetualID + ) external; + + /// @notice Safely transfers the `perpetualID` from an address to another without data in it + /// @param from Source address + /// @param to Destination a address + /// @param perpetualID ID of the perpetual to transfer + function safeTransferFrom( + address from, + address to, + uint256 perpetualID + ) external; + + /// @notice Safely transfers the `perpetualID` from an address to another with data in the transfer + /// @param from Source address + /// @param to Destination a address + /// @param perpetualID ID of the perpetual to transfer + function safeTransferFrom( + address from, + address to, + uint256 perpetualID, + bytes memory _data + ) external; +} + +/// @notice Interface for the staking contract of the Angle protocol +interface IStakingRewards { + /// @dev Used instead of having a public variable to respect the ERC20 standard + /// @return Total supply + function totalSupply() external view returns (uint256); + + /// @param account Account to query the balance of + /// @return Number of token staked by an account + function balanceOf(address account) external view returns (uint256); + + /// @return Current timestamp if a reward is being distributed and the end of the staking + /// period if staking is done + function lastTimeRewardApplicable() external view returns (uint256); + + /// @notice Returns how much unclaimed rewards an account has + /// @param account Address for which the request is made + /// @return How much a given account earned rewards + function earned(address account) external view returns (uint256); + + /// @notice Lets someone stake a given amount of `stakingTokens` + /// @param amount Amount of ERC20 staking token that the `msg.sender` wants to stake + function stake(uint256 amount) external; + + /// @notice Allows to stake on behalf of another address + /// @param amount Amount to stake + /// @param onBehalf Address to stake onBehalf of + function stakeOnBehalf(uint256 amount, address onBehalf) external; + + /// @notice Lets a user withdraw a given amount of collateral from the staking contract + /// @param amount Amount of the ERC20 staking token that the `msg.sender` wants to withdraw + function withdraw(uint256 amount) external; + + /// @notice Triggers a payment of the reward earned to the msg.sender + function getReward() external; + + /// @notice Lets the caller withdraw its staking and claim rewards + function exit() external; +} + +/// @notice Interface for agToken, that is to say Angle's stablecoins +/// @dev This contract is used to create and handle the stablecoins of Angle protocol +/// @dev Only the `StableMaster` contract can mint or burn agTokens +/// @dev It is still possible for any address to burn its agTokens without redeeming collateral in exchange +/// @dev agTokens are classical ERC-20 tokens, so it is still possible to `approve` an address, `transfer` or +/// `transferFrom` the tokens +interface IAgToken { + /// @notice Burns `amount` of agToken on behalf of another account without redeeming collateral back + /// @param account Account to burn on behalf of + /// @param amount Amount to burn + /// @param poolManager Reference to the `PoolManager` contract for which the `stocksUsers` will + /// need to be updated + /// @dev When calling this function, people should specify the `poolManager` for which they want to decrease + /// the `stocksUsers`: this a way for the protocol to maintain healthy accounting variables + /// @dev This function is for instance to be used by governance to burn the tokens accumulated by the `BondingCurve` + /// contract + function burnFromNoRedeem( + address account, + uint256 amount, + address poolManager + ) external; + + /// @notice Destroys `amount` token from the caller without giving collateral back + /// @param amount Amount to burn + /// @param poolManager Reference to the `PoolManager` contract for which the `stocksUsers` will need to be updated + function burnNoRedeem(uint256 amount, address poolManager) external; +} + +/// @notice Interface for sanTokens, these tokens are used to mark the debt the contract has to SLPs +/// @dev The exchange rate between sanTokens and collateral will automatically change as interests and transaction fees accrue to SLPs +/// @dev There is one `SanToken` contract per pair stablecoin/collateral +/// @dev Only the `StableMaster` contract can mint or burn sanTokens +/// @dev It is still possible for any address to burn its sanTokens without redeeming collateral in exchange +/// @dev Like `AgTokens`, sanTokens are classical ERC-20 tokens, so it is still possible to `approve` an address, `transfer` or +/// `transferFrom` the tokens +interface ISanToken { + /// @notice Destroys `amount` token for the caller without giving collateral back + /// @param amount Amount to burn + function burnNoRedeem(uint256 amount) external; +} + +/// @notice Interface for the `Core` contract +interface ICore { + /// @return `_governorList` List of all the governor addresses of the protocol + function governorList() external view returns (address[] memory); +} + +/// @notice Interface for Angle's oracle contracts reading oracle rates from both UniswapV3 and Chainlink, +/// from just UniswapV3 or from just Chainlink +interface IOracle { + /// @notice Reads one of the rates from the circuits given + /// @return rate The current rate between the in-currency and out-currency + /// @dev By default if the oracle involves a Uniswap price and a Chainlink price + /// this function will return the Uniswap price + /// @dev The rate returned is expressed with base `BASE` (and not the base of the out-currency) + function read() external view returns (uint256 rate); + + /// @notice Read rates from the circuit of both Uniswap and Chainlink if there are both circuits + /// else returns twice the same price + /// @return Return all available rates (Chainlink and Uniswap) with the lowest rate returned first. + /// @dev The rate returned is expressed with base `BASE` (and not the base of the out-currency) + function readAll() external view returns (uint256, uint256); + + /// @notice Reads rates from the circuit of both Uniswap and Chainlink if there are both circuits + /// and returns either the highest of both rates or the lowest + /// @return rate The lower rate between Chainlink and Uniswap + /// @dev If there is only one rate computed in an oracle contract, then the only rate is returned + /// regardless of the value of the `lower` parameter + /// @dev The rate returned is expressed with base `BASE` (and not the base of the out-currency) + function readLower() external view returns (uint256 rate); + + /// @notice Reads rates from the circuit of both Uniswap and Chainlink if there are both circuits + /// and returns either the highest of both rates or the lowest + /// @return rate The upper rate between Chainlink and Uniswap + /// @dev If there is only one rate computed in an oracle contract, then the only rate is returned + /// regardless of the value of the `lower` parameter + /// @dev The rate returned is expressed with base `BASE` (and not the base of the out-currency) + function readUpper() external view returns (uint256 rate); + + /// @notice Converts an in-currency quote amount to out-currency using one of the rates available in the oracle + /// contract + /// @param quoteAmount Amount (in the input collateral) to be converted to be converted in out-currency + /// @return Quote amount in out-currency from the base amount in in-currency + /// @dev Like in the read function, if the oracle involves a Uniswap and a Chainlink price, this function + /// will use the Uniswap price to compute the out quoteAmount + /// @dev The rate returned is expressed with base `BASE` (and not the base of the out-currency) + function readQuote(uint256 quoteAmount) external view returns (uint256); + + /// @notice Returns the lowest quote amount between Uniswap and Chainlink circuits (if possible). If the oracle + /// contract only involves a single feed, then this returns the value of this feed + /// @param quoteAmount Amount (in the input collateral) to be converted + /// @return The lowest quote amount from the quote amount in in-currency + /// @dev The rate returned is expressed with base `BASE` (and not the base of the out-currency) + function readQuoteLower(uint256 quoteAmount) external view returns (uint256); +} + +/// @notice Interface for the `BondingCurve` contract +/// @dev This contract allows people to buy ANGLE governance tokens using the protocol's stablecoins +/// @dev It is with high certainty not going to be distributed directly at launch +interface IBondingCurve { + /// @notice Lets `msg.sender` buy tokens (ANGLE tokens normally) against an allowed token (a stablecoin normally) + /// @param _agToken Reference to the agToken used, that is the stablecoin used to buy the token associated to this + /// bonding curve + /// @param maxAmountToPayInAgToken Maximum amount to pay in agTokens that the user is willing to pay to buy the + /// `targetSoldTokenQuantity` + function buySoldToken( + IAgToken _agToken, + uint256 targetSoldTokenQuantity, + uint256 maxAmountToPayInAgToken + ) external; + + /// @dev More generally than the expression used, the value of the price is: + /// `startPrice/(1-tokensSoldInTx/tokensToSellInTotal)^power` with `power = 2` + /// @dev The precision of this function is not that important as it is a view function anyone can query + /// @notice Returns the current price of the token (expressed in reference) + function getCurrentPrice() external view returns (uint256); + + /// @return The quantity of governance tokens that are still to be sold + function getQuantityLeftToSell() external view returns (uint256); + + /// @param targetQuantity Quantity of ANGLE tokens to buy + /// @dev This is an utility function that can be queried before buying tokens + /// @return The amount to pay for the desired amount of ANGLE to buy + function computePriceFromQuantity(uint256 targetQuantity) external view returns (uint256); +} + +/// @title ICollateralSettler +/// @notice Interface for the collateral settlement contracts that are used when a collateral is getting revoked +interface ICollateralSettler { + /// @notice Allows a user to claim collateral for a `dest` address by sending agTokens and gov tokens (optional) + /// @param dest Address of the user to claim collateral for + /// @param amountAgToken Amount of agTokens sent + /// @param amountGovToken Amount of governance sent + /// @dev The more gov tokens a user sends, the more preferably it ends up being treated during the redeem period + function claimUser( + address dest, + uint256 amountAgToken, + uint256 amountGovToken + ) external; + + /// @notice Allows a HA to claim collateral by sending a `perpetualID` and gov tokens (optional) + /// @param perpetualID Perpetual owned by the HA + /// @param amountGovToken Amount of governance sent + /// @dev The contract automatically recognizes the beneficiary of the perpetual + function claimHA(uint256 perpetualID, uint256 amountGovToken) external; + + /// @notice Allows a SLP to claim collateral for an address `dest` by sending sanTokens and gov tokens (optional) + /// @param dest Address to claim collateral for + /// @param amountSanToken Amount of sanTokens sent + /// @param amountGovToken Amount of governance tokens sent + function claimSLP( + address dest, + uint256 amountSanToken, + uint256 amountGovToken + ) external; + + /// @notice Computes the base amount each category of claim will get after the claim period has ended + /// @dev This function can only be called once when claim period is over + /// @dev It is at the level of this function that the waterfall between the different + /// categories of stakeholders and of claims is executed + function setAmountToRedistributeEach() external; + + /// @notice Lets a user or a LP redeem its corresponding share of collateral + /// @param user Address of the user to redeem collateral to + /// @dev This function can only be called after the `setAmountToRedistributeEach` function has been called + /// @dev The entry point to redeem is the same for users, HAs and SLPs + function redeemCollateral(address user) external; +} + +interface ILiquidityGauge { + function deposit(uint256 _value, address _addr) external; + + function withdraw(uint256 _value) external; + + function claim_rewards(address _addr) external; + + function balanceOf(address account) external view returns (uint256); +} diff --git a/angle/contracts/interfaces/IController.sol b/angle/contracts/interfaces/IController.sol new file mode 100644 index 0000000..649432b --- /dev/null +++ b/angle/contracts/interfaces/IController.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} diff --git a/angle/contracts/interfaces/IMultiRewards.sol b/angle/contracts/interfaces/IMultiRewards.sol new file mode 100644 index 0000000..a116074 --- /dev/null +++ b/angle/contracts/interfaces/IMultiRewards.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +interface IMultiRewards { + function balanceOf(address) external returns(uint); + function stakeFor(address, uint) external; + function withdrawFor(address, uint) external; + function notifyRewardAmount(address, uint) external; +} \ No newline at end of file diff --git a/angle/contracts/interfaces/IStrategy.sol b/angle/contracts/interfaces/IStrategy.sol new file mode 100644 index 0000000..2dd8d3f --- /dev/null +++ b/angle/contracts/interfaces/IStrategy.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +interface IStrategy { + function want() external view returns (address); + + function deposit() external; + + function withdraw(address) external; + + function withdraw(uint256) external; + + function withdrawAll() external returns (uint256); + + function balanceOf() external view returns (uint256); +} diff --git a/angle/contracts/strategies/NewStrategyAngleStakeDao.sol b/angle/contracts/strategies/NewStrategyAngleStakeDao.sol new file mode 100644 index 0000000..e428013 --- /dev/null +++ b/angle/contracts/strategies/NewStrategyAngleStakeDao.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +import "../BaseStrategy.sol"; +import "../interfaces/IAngle.sol"; +import "../interfaces/IMultiRewards.sol"; + +contract NewStrategyAngleStakeDao is BaseStrategy { + address public stableMaster = 0x5adDc89785D75C86aB939E9e15bfBBb7Fc086A87; + address public poolManager = 0xe9f183FC656656f1F17af1F2b0dF79b8fF9ad8eD; + address public liquidityGauge = 0x51fE22abAF4a26631b2913E417c0560D547797a7; + address public sanUSDC_EUR = 0x9C215206Da4bf108aE5aEEf9dA7caD3352A36Dad; + address public angle = 0x31429d1856aD1377A8A0079410B297e1a9e214c2; + address public gauge; + + constructor( + address _controller, + address _want, + address _gauge + ) BaseStrategy(_controller, _want) { + gauge = _gauge; + IERC20(angle).approve(_gauge, type(uint256).max); + IERC20(want).approve(stableMaster, type(uint256).max); + IERC20(sanUSDC_EUR).approve(liquidityGauge, type(uint256).max); + } + + function name() external pure override returns (string memory) { + return "StrategyAngleStakeDao"; + } + + function deposit() public override { + // usdc => sanUSDC_EUR + uint256 wantBalance = IERC20(want).balanceOf(address(this)); + IStableMaster(stableMaster).deposit(wantBalance, address(this), IPoolManager(poolManager)); + uint256 sanUsdcEurBalance = IERC20(sanUSDC_EUR).balanceOf(address(this)); + IERC20(sanUSDC_EUR).transfer(IController(controller).vaults(want), sanUsdcEurBalance); + } + + function withdraw(uint256 _amount) external override onlyController { + // Withdraw san LP from angle yield staking pool + ILiquidityGauge(liquidityGauge).withdraw(_amount); + uint256 sanLPObtained = IERC20(sanUSDC_EUR).balanceOf(address(this)); + // burn san LP to obtain USDC + IStableMaster(stableMaster).withdraw(sanLPObtained, address(this), address(this), IPoolManager(poolManager)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + uint256 usdcAmount = IERC20(want).balanceOf(address(this)); + IERC20(want).transfer(_vault, usdcAmount); + } + + function _withdrawSome(uint256 _amount) internal override { + ILiquidityGauge(liquidityGauge).withdraw(_amount); + } + + function withdrawAll() external override onlyController returns (uint256) { + uint256 stakedBalance = balanceOfPool(); + _withdrawSome(stakedBalance); + uint256 sanLPObtained = IERC20(sanUSDC_EUR).balanceOf(address(this)); + IERC20(sanUSDC_EUR).transfer(IController(controller).vaults(want), sanLPObtained); // send funds to vault + IERC20(sanUSDC_EUR).approve(liquidityGauge, 0); + return sanLPObtained; + } + + function harvest() public onlyAdmin { + // claim angle from angle + // send to multi rewards + ILiquidityGauge(liquidityGauge).claim_rewards(address(this)); + uint256 angleBalance = IERC20(angle).balanceOf(address(this)); + if (angleBalance > 0) { + uint256 _fee = (angleBalance * performanceFee) / FEE_DENOMINATOR; + IERC20(angle).transfer(IController(controller).rewards(), _fee); + uint256 angleLeft = IERC20(angle).balanceOf(address(this)); + IMultiRewards(gauge).notifyRewardAmount(angle, angleLeft); + } + } + + function stake() public { + uint256 sanUSDC_EURBalance = IERC20(sanUSDC_EUR).balanceOf(address(this)); + ILiquidityGauge(liquidityGauge).deposit(sanUSDC_EURBalance, address(this)); + } + + function balanceOfPool() public view override returns (uint256) { + return ILiquidityGauge(liquidityGauge).balanceOf(address(this)); + } + + function setGauge(address _newGauge) external onlyAdmin { + IERC20(angle).approve(gauge, 0); + gauge = _newGauge; + IERC20(angle).approve(_newGauge, type(uint256).max); + } + + function setLiquidityGauge(address _newLiquidityGauge) external onlyAdmin { + uint256 stakedBalance = balanceOfPool(); + // withdraw all from the old staking contract + _withdrawSome(stakedBalance); + // sett new staking contract + liquidityGauge = _newLiquidityGauge; + IERC20(sanUSDC_EUR).approve(_newLiquidityGauge, type(uint256).max); + // stake all into the new contract + stake(); + } + + function setPoolManager(address _newPoolManager) external onlyAdmin { + poolManager = _newPoolManager; + } + + function refreshApproves() external onlyAdmin { + IERC20(angle).approve(gauge, type(uint256).max); + IERC20(want).approve(stableMaster, type(uint256).max); + IERC20(sanUSDC_EUR).approve(liquidityGauge, type(uint256).max); + } +} diff --git a/angle/contracts/strategies/StrategyAngleStakeDao.sol b/angle/contracts/strategies/StrategyAngleStakeDao.sol new file mode 100644 index 0000000..cf84393 --- /dev/null +++ b/angle/contracts/strategies/StrategyAngleStakeDao.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +import "../BaseStrategy.sol"; +import "../interfaces/IAngle.sol"; +import "../interfaces/IMultiRewards.sol"; + +import "hardhat/console.sol"; + +contract StrategyAngleStakeDao is BaseStrategy { + address public stableMaster = 0x5adDc89785D75C86aB939E9e15bfBBb7Fc086A87; + address public poolManager = 0xe9f183FC656656f1F17af1F2b0dF79b8fF9ad8eD; + address public staking = 0x2Fa1255383364F6e17Be6A6aC7A56C9aCD6850a3; + address public sanUSDC_EUR = 0x9C215206Da4bf108aE5aEEf9dA7caD3352A36Dad; + address public angle = 0x31429d1856aD1377A8A0079410B297e1a9e214c2; + address public gauge; + + constructor( + address _controller, + address _want, + address _gauge + ) BaseStrategy(_controller, _want) { + gauge = _gauge; + IERC20(angle).approve(_gauge, type(uint256).max); + IERC20(want).approve(stableMaster, type(uint256).max); + IERC20(sanUSDC_EUR).approve(staking, type(uint256).max); + } + + function name() external pure override returns (string memory) { + return "StrategyAngleStakeDao"; + } + + function deposit() public override { + // usdc => sanUSDC_EUR + uint256 wantBalance = IERC20(want).balanceOf(address(this)); + IStableMaster(stableMaster).deposit(wantBalance, address(this), IPoolManager(poolManager)); + uint256 sanUsdcEurBalance = IERC20(sanUSDC_EUR).balanceOf(address(this)); + IERC20(sanUSDC_EUR).transfer(IController(controller).vaults(want), sanUsdcEurBalance); + } + + function withdraw(uint256 _amount) external override onlyController { + // Withdraw san LP from angle yield staking pool + IStakingRewards(staking).withdraw(_amount); + uint256 sanLPObtained = IERC20(sanUSDC_EUR).balanceOf(address(this)); + // burn san LP to obtain USDC + IStableMaster(stableMaster).withdraw(sanLPObtained, address(this), address(this), IPoolManager(poolManager)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + uint256 usdcAmount = IERC20(want).balanceOf(address(this)); + IERC20(want).transfer(_vault, usdcAmount); + } + + function _withdrawSome(uint256 _amount) internal override { + IStakingRewards(staking).withdraw(_amount); + } + + function withdrawAll() external override onlyController returns (uint256) { + uint256 stakedBalance = balanceOfPool(); + _withdrawSome(stakedBalance); + uint256 sanLPObtained = IERC20(sanUSDC_EUR).balanceOf(address(this)); + IERC20(sanUSDC_EUR).transfer(IController(controller).vaults(want), sanLPObtained); // send funds to vault + IERC20(sanUSDC_EUR).approve(staking, 0); + return sanLPObtained; + } + + function harvest() public onlyAdmin { + // claim angle from angle + // send to multi rewards + IStakingRewards(staking).getReward(); + uint256 angleBalance = IERC20(angle).balanceOf(address(this)); + if (angleBalance > 0) { + uint256 _fee = (angleBalance * performanceFee) / FEE_DENOMINATOR; + IERC20(angle).transfer(IController(controller).rewards(), _fee); + uint256 angleLeft = IERC20(angle).balanceOf(address(this)); + IMultiRewards(gauge).notifyRewardAmount(angle, angleLeft); + } + } + + function stake() public { + uint256 sanUSDC_EURBalance = IERC20(sanUSDC_EUR).balanceOf(address(this)); + IStakingRewards(staking).stake(sanUSDC_EURBalance); + } + + function balanceOfPool() public view override returns (uint256) { + return IStakingRewards(staking).balanceOf(address(this)); + } + + function setGauge(address _newGauge) external onlyAdmin { + IERC20(angle).approve(gauge, 0); + gauge = _newGauge; + IERC20(angle).approve(_newGauge, type(uint256).max); + } + + function setStaking(address _newStaking) external onlyAdmin { + uint256 stakedBalance = balanceOfPool(); + // withdraw all from the old staking contract + _withdrawSome(stakedBalance); + // sett new staking contract + staking = _newStaking; + IERC20(sanUSDC_EUR).approve(_newStaking, type(uint256).max); + // stake all into the new contract + stake(); + } + + function setPoolManager(address _newPoolManager) external onlyAdmin { + poolManager = _newPoolManager; + } + + function refreshApproves() external onlyAdmin { + IERC20(angle).approve(gauge, type(uint256).max); + IERC20(want).approve(stableMaster, type(uint256).max); + IERC20(sanUSDC_EUR).approve(staking, type(uint256).max); + } +} diff --git a/angle/deposit_flow.png b/angle/deposit_flow.png new file mode 100644 index 0000000..830f3c4 Binary files /dev/null and b/angle/deposit_flow.png differ diff --git a/angle/hardhat.config.ts b/angle/hardhat.config.ts new file mode 100644 index 0000000..aeaff1c --- /dev/null +++ b/angle/hardhat.config.ts @@ -0,0 +1,57 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomiclabs/hardhat-waffle"; +import "@nomiclabs/hardhat-etherscan"; +import "@nomiclabs/hardhat-vyper"; +import "hardhat-gas-reporter"; +import "solidity-coverage"; +import "hardhat-deploy"; +import "hardhat-deploy-ethers"; +import "hardhat-contract-sizer"; +import "./tasks/global"; + +require("dotenv").config(); + +export default { + defaultNetwork: "hardhat", + networks: { + hardhat: { + forking: { + url: process.env.MAINNET, + blockNumber: 14036501 + } + }, + mainnet: { + url: process.env.MAINNET, + accounts: [`0x${process.env.DEPLOYER_PKEY}`], + gasPrice: 120000000000 + } + }, + namedAccounts: { + deployer: 0 + }, + vyper: { + version: "0.2.7" + }, + solidity: { + compilers: [{ version: "0.8.2" }, { version: "0.7.4" }, { version: "0.6.12" }, { version: "0.5.17" }], + settings: { + optimizer: { + enabled: true, + runs: 200 + } + } + }, + etherscan: { + apiKey: process.env.ETHERSCAN_KEY + }, + contractSizer: { + alphaSort: true, + disambiguatePaths: false, + runOnCompile: true, + strict: false + }, + gasReporter: { + currency: "USD", + coinmarketcap: process.env.COINMARKETCAP_KEY + } +} as HardhatUserConfig; diff --git a/angle/migration_flows.png b/angle/migration_flows.png new file mode 100644 index 0000000..ad1b38d Binary files /dev/null and b/angle/migration_flows.png differ diff --git a/angle/test/fixtures/Controller.json b/angle/test/fixtures/Controller.json new file mode 100644 index 0000000..211ac3f --- /dev/null +++ b/angle/test/fixtures/Controller.json @@ -0,0 +1,320 @@ +[ + { + "inputs": [ + {"internalType": "address", "name": "_rewards", "type": "address"} + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "approveStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "approvedStrategies", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "converters", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "earn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "parts", "type": "uint256"} + ], + "name": "getExpectedReturn", + "outputs": [ + {"internalType": "uint256", "name": "expected", "type": "uint256"} + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "governance", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "inCaseStrategyTokenGetStuck", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "inCaseTokensGetStuck", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "max", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "onesplit", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "revokeStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "rewards", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_input", "type": "address"}, + {"internalType": "address", "name": "_output", "type": "address"}, + {"internalType": "address", "name": "_converter", "type": "address"} + ], + "name": "setConverter", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_governance", "type": "address"} + ], + "name": "setGovernance", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_onesplit", "type": "address"} + ], + "name": "setOneSplit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_rewards", "type": "address"} + ], + "name": "setRewards", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_split", "type": "uint256"} + ], + "name": "setSplit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategist", "type": "address"} + ], + "name": "setStrategist", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "setStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_vault", "type": "address"} + ], + "name": "setVault", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "split", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "strategies", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "strategist", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "vaults", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "withdrawAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "parts", "type": "uint256"} + ], + "name": "yearn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } + ] \ No newline at end of file diff --git a/angle/test/fixtures/ERC20.json b/angle/test/fixtures/ERC20.json new file mode 100644 index 0000000..d7661a4 --- /dev/null +++ b/angle/test/fixtures/ERC20.json @@ -0,0 +1,152 @@ +[ + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "subtractedValue", "type": "uint256" } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "addedValue", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_to", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/angle/test/migration.ts b/angle/test/migration.ts new file mode 100644 index 0000000..e0d40a5 --- /dev/null +++ b/angle/test/migration.ts @@ -0,0 +1,146 @@ +import { ethers, network } from "hardhat"; +import { expect } from "chai"; +import { BigNumber } from "@ethersproject/bignumber"; +import { Contract, ContractTransaction } from "@ethersproject/contracts"; +import { parseEther } from "@ethersproject/units"; +import { JsonRpcSigner } from "@ethersproject/providers"; + +import Controller from "./fixtures/Controller.json"; +import ERC20 from "./fixtures/ERC20.json"; +import { parse } from "path/posix"; + +const CONTROLLER = "0x29D3782825432255041Db2EAfCB7174f5273f08A"; +const CONTROLLER_ADMIN = "0xf930ebbd05ef8b25b1797b9b2109ddc9b0d43063"; +const WANT = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC +const POOL_MANAGER = "0xe9f183FC656656f1F17af1F2b0dF79b8fF9ad8eD"; +const STABLE_MASTER = "0x5adDc89785D75C86aB939E9e15bfBBb7Fc086A87"; +const GOVERNANCE = "0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063"; +const WANT_HOLDER = "0x0DAFB4114762bDf555d9c6BDa02f4ffEc89964ec"; // USDC holder +const SANUSDC_EUR = "0x9C215206Da4bf108aE5aEEf9dA7caD3352A36Dad"; +const ANGLE = "0x31429d1856aD1377A8A0079410B297e1a9e214c2"; +const SDT = "0x73968b9a57c6E53d41345FD57a6E6ae27d6CDB2F"; +const SDT_HOLDER = "0x026B748689c3a4363ca044A323D5F405fe16a750"; +const AGEUR = "0x1a7e4e63778B4f12a199C062f3eFdD288afCBce8"; +const PROXY = "0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A"; +const STAKING_REWARDS = "0x2Fa1255383364F6e17Be6A6aC7A56C9aCD6850a3"; +const STAKEDAO_DEPLOYER = "0xb36a0671b3d49587236d7833b01e79798175875f"; +describe("ANGLE USDC strat", function () { + let controller: Contract; + let strategy: Contract; + let strategyNew: Contract; + let vault: Contract; + let want: Contract; + let angle: Contract; + let sdt: Contract; + let sanLP: Contract; + let stableMaster: Contract; + let controllerAdmin: JsonRpcSigner; + let wantHolder: JsonRpcSigner; + let sdtHolder: JsonRpcSigner; + let governance: JsonRpcSigner; + let controllerSigner: JsonRpcSigner; + let ageur: Contract; + let gauge: Contract; + let stakingRewards: Contract; + let liquidityGauge: Contract; + let vaultGovernance: JsonRpcSigner; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [GOVERNANCE] + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [WANT_HOLDER] + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [CONTROLLER] + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [SDT_HOLDER] + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [CONTROLLER_ADMIN] + }); + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [STAKEDAO_DEPLOYER] + }); + await network.provider.send("hardhat_setBalance", [STAKEDAO_DEPLOYER, "0x56BC75E2D63100000"]); + await network.provider.send("hardhat_setBalance", [WANT_HOLDER, "0x56BC75E2D63100000"]); + governance = await ethers.provider.getSigner(GOVERNANCE); + wantHolder = await ethers.provider.getSigner(WANT_HOLDER); + vaultGovernance = await ethers.provider.getSigner(STAKEDAO_DEPLOYER); + sdtHolder = await ethers.provider.getSigner(SDT_HOLDER); + controllerAdmin = await ethers.provider.getSigner(CONTROLLER_ADMIN); + controllerSigner = await ethers.provider.getSigner(CONTROLLER); + controller = await ethers.getContractAt(Controller, CONTROLLER); + vault = await ethers.getContractAt("Vault", "0xf3c2bdfCCb75CAFdA3D69d807c336bede956563f"); + stableMaster = await ethers.getContractAt("IStableMaster", STABLE_MASTER); + stakingRewards = await ethers.getContractAt("IStakingRewards", STAKING_REWARDS); + ageur = await ethers.getContractAt("IERC20", AGEUR); + gauge = await ethers.getContractAt("GaugeMultiRewards", "0x3c310fc54c0534dc3c45312934508722284352d1"); + strategy = await ethers.getContractAt("StrategyAngleStakeDao", "0x9eEF1244AE7aeeDeAa3Df2a91B63eAABC4Fce257"); + liquidityGauge = await ethers.getContractAt("ILiquidityGauge", "0x51fE22abAF4a26631b2913E417c0560D547797a7"); + want = await ethers.getContractAt(ERC20, WANT); + sanLP = await ethers.getContractAt(ERC20, SANUSDC_EUR); + angle = await ethers.getContractAt(ERC20, ANGLE); + sdt = await ethers.getContractAt(ERC20, SDT); + + await ageur.connect(wantHolder).approve(stableMaster.address, ethers.constants.MaxUint256); + await want.connect(wantHolder).approve(vault.address, ethers.constants.MaxUint256); + }); + + it("it should migrate the strat", async function () { + this.enableTimeouts(false); + const newStratFactory = await ethers.getContractFactory("NewStrategyAngleStakeDao"); + strategyNew = await newStratFactory.deploy(controller.address, want.address, gauge.address); + const sanLpBalanceOfVaultBeforeWithdrawAll = await sanLP.balanceOf(vault.address); + await (await controller.connect(governance).approveStrategy(want.address, strategyNew.address)).wait(); + await (await controller.connect(governance).setStrategy(want.address, strategyNew.address)).wait(); + const sanLpBalanceOfVaultAfterWithdrawAll = await sanLP.balanceOf(vault.address); + expect(sanLpBalanceOfVaultAfterWithdrawAll).to.be.gt(sanLpBalanceOfVaultBeforeWithdrawAll); + }); + + it("it should stake to the new staking contract", async function () { + this.enableTimeouts(false); + const stakedAmountBeforeEarn = await liquidityGauge.balanceOf(strategyNew.address); + await (await vault.connect(vaultGovernance).earn()).wait(); + const stakedAmountAfterEarn = await liquidityGauge.balanceOf(strategyNew.address); + expect(stakedAmountBeforeEarn).to.be.equal(0); + expect(stakedAmountAfterEarn).to.be.gt(stakedAmountBeforeEarn); + }); + + it("User should deposit USDC in vault", async function () { + this.enableTimeouts(false); + const amount = BigNumber.from(10).pow(6).mul(20000); // 20k USDC + const sanLpBalanceBeforeDeposit = await gauge.balanceOf(wantHolder._address); + await vault.connect(wantHolder).deposit(amount); + const sanLpBalanceAfterDeposit = await gauge.balanceOf(wantHolder._address); + expect(sanLpBalanceAfterDeposit).to.be.gt(sanLpBalanceBeforeDeposit); // make sure it staked + }); + it("User should be able to withdraw after earn", async function () { + this.enableTimeouts(false); + const stakedAmountBeforeEarn = await liquidityGauge.balanceOf(strategyNew.address); + await (await vault.connect(vaultGovernance).earn()).wait(); + const stakedAmountAfterEarn = await liquidityGauge.balanceOf(strategyNew.address); + const sanLpBalanceBeforeWithdraw = await gauge.balanceOf(wantHolder._address); + await vault.connect(wantHolder).withdraw(sanLpBalanceBeforeWithdraw); + const sanLpBalanceAfterWithdraw = await gauge.balanceOf(wantHolder._address); + expect(stakedAmountBeforeEarn).to.be.gt(0); + expect(stakedAmountAfterEarn).to.be.gt(stakedAmountBeforeEarn); + expect(sanLpBalanceBeforeWithdraw).to.be.gt(0); + expect(sanLpBalanceAfterWithdraw).to.be.eq(0); + }); +}); diff --git a/angle/test/strategy.ts b/angle/test/strategy.ts new file mode 100644 index 0000000..ac84235 --- /dev/null +++ b/angle/test/strategy.ts @@ -0,0 +1,264 @@ +import { ethers, network } from "hardhat"; +import { expect } from "chai"; +import { BigNumber } from "@ethersproject/bignumber"; +import { Contract, ContractTransaction } from "@ethersproject/contracts"; +import { parseEther } from "@ethersproject/units"; +import { JsonRpcSigner } from "@ethersproject/providers"; + +import Controller from "./fixtures/Controller.json"; +import ERC20 from "./fixtures/ERC20.json"; +import { parse } from "path/posix"; + +const CONTROLLER = "0x29D3782825432255041Db2EAfCB7174f5273f08A"; +const CONTROLLER_ADMIN = "0xf930ebbd05ef8b25b1797b9b2109ddc9b0d43063"; +const WANT = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC +const POOL_MANAGER = "0xe9f183FC656656f1F17af1F2b0dF79b8fF9ad8eD"; +const STABLE_MASTER = "0x5adDc89785D75C86aB939E9e15bfBBb7Fc086A87"; +const GOVERNANCE = "0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063"; +const WANT_HOLDER = "0x41339d9825963515e5705df8d3b0ea98105ebb1c"; // USDC holder +const SANUSDC_EUR = "0x9C215206Da4bf108aE5aEEf9dA7caD3352A36Dad"; +const ANGLE = "0x31429d1856aD1377A8A0079410B297e1a9e214c2"; +const SDT = "0x73968b9a57c6E53d41345FD57a6E6ae27d6CDB2F"; +const SDT_HOLDER = "0x026B748689c3a4363ca044A323D5F405fe16a750"; +const AGEUR = "0x1a7e4e63778B4f12a199C062f3eFdD288afCBce8"; +const PROXY = "0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A"; +const STAKING_REWARDS = "0x2Fa1255383364F6e17Be6A6aC7A56C9aCD6850a3"; + +describe("ANGLE USDC strat", function () { + let controller: Contract; + let strategy: Contract; + let strategyNew: Contract; + let vault: Contract; + let want: Contract; + let angle: Contract; + let sdt: Contract; + let sanLP: Contract; + let stableMaster: Contract; + let controllerAdmin: JsonRpcSigner; + let wantHolder: JsonRpcSigner; + let sdtHolder: JsonRpcSigner; + let governance: JsonRpcSigner; + let controllerSigner: JsonRpcSigner; + let ageur: Contract; + let gauge: Contract; + let stakingRewards: Contract; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [GOVERNANCE] + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [WANT_HOLDER] + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [CONTROLLER] + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [SDT_HOLDER] + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [CONTROLLER_ADMIN] + }); + + const Strategy = await ethers.getContractFactory("StrategyAngleStakeDao"); + const Vault = await ethers.getContractFactory("Vault"); + const Gauge = await ethers.getContractFactory("GaugeMultiRewards"); + + governance = await ethers.provider.getSigner(GOVERNANCE); + wantHolder = await ethers.provider.getSigner(WANT_HOLDER); + sdtHolder = await ethers.provider.getSigner(SDT_HOLDER); + controllerAdmin = await ethers.provider.getSigner(CONTROLLER_ADMIN); + controllerSigner = await ethers.provider.getSigner(CONTROLLER); + controller = await ethers.getContractAt(Controller, CONTROLLER); + vault = await Vault.deploy(WANT, controller.address); + stableMaster = await ethers.getContractAt("IStableMaster", STABLE_MASTER); + stakingRewards = await ethers.getContractAt("IStakingRewards", STAKING_REWARDS); + ageur = await ethers.getContractAt("IERC20", AGEUR); + gauge = await Gauge.deploy(vault.address); + strategy = await Strategy.deploy(CONTROLLER, WANT, gauge.address); + strategyNew = await Strategy.deploy(CONTROLLER, WANT, gauge.address); + want = await ethers.getContractAt(ERC20, WANT); + sanLP = await ethers.getContractAt(ERC20, SANUSDC_EUR); + angle = await ethers.getContractAt(ERC20, ANGLE); + sdt = await ethers.getContractAt(ERC20, SDT); + await gauge.addReward(ANGLE, strategy.address, 86400 * 3); + + const tx1 = await controller.connect(governance).approveStrategy(WANT, strategy.address); + + await tx1.wait(); + + const tx2 = await controller.connect(governance).setStrategy(WANT, strategy.address); + + await tx2.wait(); + + const tx3 = await controller.connect(governance).setVault(WANT, vault.address); + await tx3.wait(); + await ageur.connect(wantHolder).approve(stableMaster.address, ethers.constants.MaxUint256); + await want.connect(wantHolder).approve(vault.address, ethers.constants.MaxUint256); + }); + + it("User should not deposit before gauge has been set", async function () { + this.enableTimeouts(false); + const amount = BigNumber.from(10).pow(6).mul(20000); // 20k USDC + + await expect(vault.connect(wantHolder).deposit(amount)).to.be.revertedWith("Gauge not yet initialized"); + }); + it("User should be able to deposit and withdraw immediately", async function () { + this.enableTimeouts(false); + await vault.setGauge(gauge.address); + const usdcBalance = await want.balanceOf(wantHolder._address); + const amount = BigNumber.from(10).pow(6).mul(20000); // 20k USDC + await vault.connect(wantHolder).deposit(amount); + const usdcBalanceAfterDeposit = await want.balanceOf(wantHolder._address); + const sanLpBalance = await gauge.balanceOf(wantHolder._address); + await vault.connect(wantHolder).withdraw(sanLpBalance); + const usdcBalanceAfterWithdraw = await want.balanceOf(wantHolder._address); + expect(usdcBalance).to.be.gt(0); + expect(usdcBalanceAfterDeposit).to.be.lt(usdcBalance); + expect(usdcBalanceAfterWithdraw).to.be.gt(usdcBalance); + }); + it("User should deposit USDC in vault", async function () { + this.enableTimeouts(false); + const amount = BigNumber.from(10).pow(6).mul(20000); // 20k USDC + const sanLpBalanceBeforeDeposit = await gauge.balanceOf(wantHolder._address); + await vault.connect(wantHolder).deposit(amount); + const sanLpBalanceAfterDeposit = await gauge.balanceOf(wantHolder._address); + expect(sanLpBalanceAfterDeposit).to.be.gt(sanLpBalanceBeforeDeposit); // make sure it staked + }); + + it("it should withdraw the partial amount of the staked amount", async function () { + this.enableTimeouts(false); + const withDrawAmountWithoutProfit = BigNumber.from(10).pow(6).mul(20000).mul(20).div(100); + const usdcBalanceOfUser = await want.balanceOf(wantHolder._address); + const sanLpBalance = (await gauge.balanceOf(wantHolder._address)).mul(20).div(100); + await vault.connect(wantHolder).withdraw(sanLpBalance); + const usdcBalanceOfUserAfterWithdraw = await want.balanceOf(wantHolder._address); + expect(usdcBalanceOfUserAfterWithdraw).to.be.gt(usdcBalanceOfUser); // make sure user get the usdc + expect(usdcBalanceOfUserAfterWithdraw.sub(usdcBalanceOfUser)).to.be.gt(withDrawAmountWithoutProfit); // make sure user gets profit + }); + + it("Should harvest", async function () { + this.enableTimeouts(false); + await vault.earn(); + await network.provider.send("evm_increaseTime", [86400 * 5]); + await network.provider.send("evm_mine", []); + await strategy.harvest(); + const angleFarmed = await angle.balanceOf(gauge.address); + expect(angleFarmed).to.be.gt(0); + }); + + it("Should get ANGLE reward from gauge, called by another address", async function () { + this.enableTimeouts(false); + + await gauge.connect(governance).getRewardFor(wantHolder._address); + const angleFarmed = await angle.balanceOf(wantHolder._address); + expect(angleFarmed).to.be.gt(0); + }); + + it("Should add new reward and notify it", async function () { + this.enableTimeouts(false); + await gauge.addReward(SDT, sdtHolder._address, 86400 * 3); + await sdt.connect(sdtHolder).approve(gauge.address, parseEther("100")); + await gauge.connect(sdtHolder).notifyRewardAmount(SDT, parseEther("100")); + await network.provider.send("evm_increaseTime", [86400 * 2]); + await network.provider.send("evm_mine", []); + }); + + it("Should migrate to the new angle staking contract", async function () { + this.enableTimeouts(false); + const balanceInOldStaking = await strategy.balanceOfPool(); + await strategy.setStaking(strategy.staking()); + const balanceInNewStaking = await strategy.balanceOfPool(); + expect(balanceInOldStaking).to.be.eq(balanceInNewStaking); + }); + + it("Should deploy new strategy", async function () { + this.enableTimeouts(false); + await controller.connect(governance).approveStrategy(WANT, strategyNew.address); + // set new strategy, it will call withdrawAll() + await controller.connect(governance).setStrategy(WANT, strategyNew.address); + const sanLPAmount = await sanLP.balanceOf(vault.address); + expect(sanLPAmount).to.be.gt(0); + // it sends again funds to the new strategy + await vault.earn(); + }); + + it("it should withdraw their usdc along with profits", async function () { + this.enableTimeouts(false); + const withDrawAmountWithoutProfit = BigNumber.from(10).pow(6).mul(20000).mul(80).div(100); + const rateBefore = await stableMaster.collateralMap(POOL_MANAGER); + await manipulateExchangeRate(); + const rateAfter = await stableMaster.collateralMap(POOL_MANAGER); + const usdcBalanceBeforeWithdraw = await want.balanceOf(wantHolder._address); + const sanLpBalance = await gauge.balanceOf(wantHolder._address); + await vault.connect(wantHolder).withdraw(sanLpBalance); + const usdcBalanceAfterWithdraw = await want.balanceOf(wantHolder._address); + const sanLpBalanceAfterWithdraw = await gauge.balanceOf(wantHolder._address); + expect(rateAfter[5]).to.be.gt(rateBefore[5]); + expect(sanLpBalanceAfterWithdraw).to.be.equal(0); + expect(usdcBalanceAfterWithdraw.sub(usdcBalanceBeforeWithdraw)).to.be.gt(withDrawAmountWithoutProfit); + }); + + it("Should get ANGLE and SDT reward after the whole withdraw", async function () { + this.enableTimeouts(false); + const sanLpBalance = await gauge.balanceOf(wantHolder._address); + expect(sanLpBalance).to.be.equal(0); + const angleBefore = await angle.balanceOf(wantHolder._address); + const sdtBefore = await sdt.balanceOf(wantHolder._address); + await gauge.connect(wantHolder).getRewardFor(wantHolder._address); + const angleAfter = await angle.balanceOf(wantHolder._address); + const sdtAfter = await sdt.balanceOf(wantHolder._address); + expect(angleAfter).to.be.gt(angleBefore); + expect(sdtAfter).to.be.gt(sdtBefore); + }); + + it("After call earn lp tokens should staked to the angle staking contract", async function () { + this.enableTimeouts(false); + const stakedAmountInAngle = await stakingRewards.balanceOf(strategyNew.address); + const amount = BigNumber.from(10).pow(6).mul(20000); // 20k USDC + await vault.connect(wantHolder).deposit(amount); + await vault.earn(); + const stakedAmountInAngleAfterEarn = await stakingRewards.balanceOf(strategyNew.address); + expect(stakedAmountInAngle).to.be.equal(0); + expect(stakedAmountInAngleAfterEarn).to.be.gt(stakedAmountInAngle); + }); + + it("Should not rescue sanLP when the vault is empty", async function () { + this.enableTimeouts(false); + await expect(vault.connect(sdtHolder).withdrawRescue()).to.be.revertedWith("Cannot withdraw 0"); + }); + + it("User should rescue sanLP from vault", async function () { + // it calls directly withdrawAll() from the controller + await controller.connect(governance).withdrawAll(WANT); + const sanLPAmount = await sanLP.balanceOf(vault.address); + expect(sanLPAmount).to.be.gt(0); + const amountBefore = await sanLP.balanceOf(wantHolder._address); + await vault.connect(wantHolder).withdrawRescue(); + const amountAfter = await sanLP.balanceOf(wantHolder._address); + const amountRescued = amountAfter.sub(amountBefore); + expect(amountRescued).to.be.gt(0); + const amountLeftInVault = await sanLP.balanceOf(vault.address); + expect(amountLeftInVault).to.be.eq(0); + }); + + async function manipulateExchangeRate() { + const amount = BigNumber.from(10).pow(6).mul(100000); + await want.connect(wantHolder).approve(stableMaster.address, amount); + await stableMaster.connect(wantHolder).mint(amount, wantHolder._address, POOL_MANAGER, 0); // make some ageur minting to increase LP token reward rate + const agEUrBal = await ageur.balanceOf(wantHolder._address); + await stableMaster.connect(wantHolder).burn(agEUrBal, wantHolder._address, wantHolder._address, POOL_MANAGER, 0); + } +}); diff --git a/angle/tsconfig.json b/angle/tsconfig.json new file mode 100644 index 0000000..7fe7de9 --- /dev/null +++ b/angle/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "resolveJsonModule": true + }, + "include": ["./scripts", "./test"], + "files": ["./hardhat.config.ts"] +} diff --git a/angle/withdraw_flow.png b/angle/withdraw_flow.png new file mode 100644 index 0000000..03430f0 Binary files /dev/null and b/angle/withdraw_flow.png differ diff --git a/avax/.gitignore b/avax/.gitignore new file mode 100644 index 0000000..1562d2c --- /dev/null +++ b/avax/.gitignore @@ -0,0 +1,5 @@ +.env +node_modules +artifacts +cache +deployments \ No newline at end of file diff --git a/avax/contracts/Controller.sol b/avax/contracts/Controller.sol new file mode 100644 index 0000000..d37292b --- /dev/null +++ b/avax/contracts/Controller.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +interface IStrategy { + function want() external view returns (address); + + function deposit() external; + + function withdraw(address) external; + + function withdraw(uint256) external; + + function withdrawAll() external returns (uint256); + + function balanceOf() external view returns (uint256); +} + +contract Controller { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public governance; + address public strategist; + + address public rewards; + mapping(address => address) public vaults; + mapping(address => address) public strategies; + + mapping(address => mapping(address => bool)) public approvedStrategies; + + uint256 public constant max = 10000; + + event SetStrategy(address indexed asset, address indexed strategy); + event ApproveStrategy(address indexed asset, address indexed strategy); + event SetVault(address indexed asset, address indexed vault); + + constructor(address _rewards) public { + governance = msg.sender; + strategist = msg.sender; + rewards = _rewards; + } + + function setRewards(address _rewards) public { + require(msg.sender == governance, '!governance'); + rewards = _rewards; + } + + function setStrategist(address _strategist) public { + require(msg.sender == governance, '!governance'); + strategist = _strategist; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, '!governance'); + governance = _governance; + } + + function setVault(address _token, address _vault) public { + require( + msg.sender == strategist || msg.sender == governance, + '!strategist' + ); + require(vaults[_token] == address(0), 'vault'); + vaults[_token] = _vault; + emit SetVault(_token, _vault); + } + + function approveStrategy(address _token, address _strategy) public { + require(msg.sender == governance, '!governance'); + approvedStrategies[_token][_strategy] = true; + emit ApproveStrategy(_token, _strategy); + } + + function revokeStrategy(address _token, address _strategy) public { + require(msg.sender == governance, '!governance'); + approvedStrategies[_token][_strategy] = false; + } + + function setStrategy(address _token, address _strategy) public { + require( + msg.sender == strategist || msg.sender == governance, + '!strategist' + ); + require(approvedStrategies[_token][_strategy] == true, '!approved'); + + address _current = strategies[_token]; + if (_current != address(0)) { + IStrategy(_current).withdrawAll(); + } + strategies[_token] = _strategy; + emit SetStrategy(_token, _strategy); + } + + function earn(address _token, uint256 _amount) public { + address _strategy = strategies[_token]; + IERC20(_token).safeTransfer(_strategy, _amount); + IStrategy(_strategy).deposit(); + } + + function balanceOf(address _token) external view returns (uint256) { + return IStrategy(strategies[_token]).balanceOf(); + } + + function withdrawAll(address _token) public { + require( + msg.sender == strategist || msg.sender == governance, + '!strategist' + ); + IStrategy(strategies[_token]).withdrawAll(); + } + + function inCaseTokensGetStuck(address _token, uint256 _amount) public { + require( + msg.sender == strategist || msg.sender == governance, + '!governance' + ); + IERC20(_token).safeTransfer(msg.sender, _amount); + } + + function inCaseStrategyTokenGetStuck(address _strategy, address _token) + public + { + require( + msg.sender == strategist || msg.sender == governance, + '!governance' + ); + IStrategy(_strategy).withdraw(_token); + } + + function withdraw(address _token, uint256 _amount) public { + require(msg.sender == vaults[_token], '!vault'); + IStrategy(strategies[_token]).withdraw(_amount); + } +} diff --git a/avax/contracts/MultiRewards.sol b/avax/contracts/MultiRewards.sol new file mode 100644 index 0000000..0f18b0e --- /dev/null +++ b/avax/contracts/MultiRewards.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/math/Math.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +contract MultiRewards is ReentrancyGuard, Pausable, Ownable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /* ========== STATE VARIABLES ========== */ + + struct Reward { + address rewardsDistributor; + uint256 rewardsDuration; + uint256 periodFinish; + uint256 rewardRate; + uint256 lastUpdateTime; + uint256 rewardPerTokenStored; + } + IERC20 public stakingToken; + mapping(address => Reward) public rewardData; + address[] public rewardTokens; + + // user -> reward token -> amount + mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid; + mapping(address => mapping(address => uint256)) public rewards; + + uint256 private _totalSupply; + mapping(address => uint256) private _balances; + + /* ========== CONSTRUCTOR ========== */ + + constructor( + address _stakingToken + ) public { + stakingToken = IERC20(_stakingToken); + } + + function addReward( + address _rewardsToken, + address _rewardsDistributor, + uint256 _rewardsDuration + ) + public + onlyOwner + { + require(rewardData[_rewardsToken].rewardsDuration == 0); + rewardTokens.push(_rewardsToken); + rewardData[_rewardsToken].rewardsDistributor = _rewardsDistributor; + rewardData[_rewardsToken].rewardsDuration = _rewardsDuration; + } + + /* ========== VIEWS ========== */ + + function totalSupply() external view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) external view returns (uint256) { + return _balances[account]; + } + + function lastTimeRewardApplicable(address _rewardsToken) public view returns (uint256) { + return Math.min(block.timestamp, rewardData[_rewardsToken].periodFinish); + } + + function rewardPerToken(address _rewardsToken) public view returns (uint256) { + if (_totalSupply == 0) { + return rewardData[_rewardsToken].rewardPerTokenStored; + } + return + rewardData[_rewardsToken].rewardPerTokenStored.add( + lastTimeRewardApplicable(_rewardsToken).sub(rewardData[_rewardsToken].lastUpdateTime).mul(rewardData[_rewardsToken].rewardRate).mul(1e18).div(_totalSupply) + ); + } + + function earned(address account, address _rewardsToken) public view returns (uint256) { + return _balances[account].mul(rewardPerToken(_rewardsToken).sub(userRewardPerTokenPaid[account][_rewardsToken])).div(1e18).add(rewards[account][_rewardsToken]); + } + + function getRewardForDuration(address _rewardsToken) external view returns (uint256) { + return rewardData[_rewardsToken].rewardRate.mul(rewardData[_rewardsToken].rewardsDuration); + } + + /* ========== MUTATIVE FUNCTIONS ========== */ + + function setRewardsDistributor(address _rewardsToken, address _rewardsDistributor) external onlyOwner { + rewardData[_rewardsToken].rewardsDistributor = _rewardsDistributor; + } + + function stake(uint256 amount) external nonReentrant whenNotPaused updateReward(msg.sender) { + require(amount > 0, "Cannot stake 0"); + _totalSupply = _totalSupply.add(amount); + _balances[msg.sender] = _balances[msg.sender].add(amount); + stakingToken.safeTransferFrom(msg.sender, address(this), amount); + emit Staked(msg.sender, amount); + } + + function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) { + require(amount > 0, "Cannot withdraw 0"); + _totalSupply = _totalSupply.sub(amount); + _balances[msg.sender] = _balances[msg.sender].sub(amount); + stakingToken.safeTransfer(msg.sender, amount); + emit Withdrawn(msg.sender, amount); + } + + function getReward() public nonReentrant updateReward(msg.sender) { + + for (uint i; i < rewardTokens.length; i++) { + address _rewardsToken = rewardTokens[i]; + uint256 reward = rewards[msg.sender][_rewardsToken]; + if (reward > 0) { + rewards[msg.sender][_rewardsToken] = 0; + IERC20(_rewardsToken).safeTransfer(msg.sender, reward); + emit RewardPaid(msg.sender, _rewardsToken, reward); + } + } + } + + function exit() external { + withdraw(_balances[msg.sender]); + getReward(); + } + + /* ========== RESTRICTED FUNCTIONS ========== */ + + function notifyRewardAmount(address _rewardsToken, uint256 reward) external updateReward(address(0)) { + require(rewardData[_rewardsToken].rewardsDistributor == msg.sender); + // handle the transfer of reward tokens via `transferFrom` to reduce the number + // of transactions required and ensure correctness of the reward amount + IERC20(_rewardsToken).safeTransferFrom(msg.sender, address(this), reward); + + if (block.timestamp >= rewardData[_rewardsToken].periodFinish) { + rewardData[_rewardsToken].rewardRate = reward.div(rewardData[_rewardsToken].rewardsDuration); + } else { + uint256 remaining = rewardData[_rewardsToken].periodFinish.sub(block.timestamp); + uint256 leftover = remaining.mul(rewardData[_rewardsToken].rewardRate); + rewardData[_rewardsToken].rewardRate = reward.add(leftover).div(rewardData[_rewardsToken].rewardsDuration); + } + + rewardData[_rewardsToken].lastUpdateTime = block.timestamp; + rewardData[_rewardsToken].periodFinish = block.timestamp.add(rewardData[_rewardsToken].rewardsDuration); + emit RewardAdded(reward); + } + + // Added to support recovering LP Rewards from other systems such as BAL to be distributed to holders + function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyOwner { + require(tokenAddress != address(stakingToken), "Cannot withdraw staking token"); + require(rewardData[tokenAddress].lastUpdateTime == 0, "Cannot withdraw reward token"); + IERC20(tokenAddress).safeTransfer(owner(), tokenAmount); + emit Recovered(tokenAddress, tokenAmount); + } + + function setRewardsDuration(address _rewardsToken, uint256 _rewardsDuration) external { + require( + block.timestamp > rewardData[_rewardsToken].periodFinish, + "Reward period still active" + ); + require(rewardData[_rewardsToken].rewardsDistributor == msg.sender); + require(_rewardsDuration > 0, "Reward duration must be non-zero"); + rewardData[_rewardsToken].rewardsDuration = _rewardsDuration; + emit RewardsDurationUpdated(_rewardsToken, rewardData[_rewardsToken].rewardsDuration); + } + + /* ========== MODIFIERS ========== */ + + modifier updateReward(address account) { + for (uint i; i < rewardTokens.length; i++) { + address token = rewardTokens[i]; + rewardData[token].rewardPerTokenStored = rewardPerToken(token); + rewardData[token].lastUpdateTime = lastTimeRewardApplicable(token); + if (account != address(0)) { + rewards[account][token] = earned(account, token); + userRewardPerTokenPaid[account][token] = rewardData[token].rewardPerTokenStored; + } + } + _; + } + + /* ========== EVENTS ========== */ + + event RewardAdded(uint256 reward); + event Staked(address indexed user, uint256 amount); + event Withdrawn(address indexed user, uint256 amount); + event RewardPaid(address indexed user, address indexed rewardsToken, uint256 reward); + event RewardsDurationUpdated(address token, uint256 newDuration); + event Recovered(address token, uint256 amount); +} \ No newline at end of file diff --git a/avax/contracts/Vault.sol b/avax/contracts/Vault.sol new file mode 100644 index 0000000..80ea2cf --- /dev/null +++ b/avax/contracts/Vault.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +contract Vault is ERC20 { + using SafeERC20 for ERC20; + using Address for address; + using SafeMath for uint256; + + ERC20 public token; + + uint256 public min = 10000; + uint256 public constant max = 10000; + + address public governance; + address public controller; + + event Deposit(address indexed _from, uint256 _shares, uint256 _amount, uint256 pps); + + event Withdraw(address indexed _from, uint256 _shares, uint256 _amount, uint256 pps); + + event Earn(address indexed _from, uint256 _amount); + + constructor( + address _token, + address _controller, + address _governance, + string memory _namePrefix, + string memory _symbolPrefix + ) + public + ERC20( + string(abi.encodePacked(_namePrefix, ERC20(_token).name())), + string(abi.encodePacked(_symbolPrefix, ERC20(_token).symbol())) + ) + { + token = ERC20(_token); + controller = _controller; + governance = _governance; + } + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + function decimals() public view virtual override returns (uint8) { + return token.decimals(); + } + + function balance() public view returns (uint256) { + return token.balanceOf(address(this)).add(IController(controller).balanceOf(address(token))); + } + + function setMin(uint256 _min) external onlyGovernance { + min = _min; + } + + function setGovernance(address _governance) public onlyGovernance { + governance = _governance; + } + + function setController(address _controller) public onlyGovernance { + controller = _controller; + } + + function available() public view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() public { + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + emit Earn(msg.sender, _bal); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint256 _amount) public { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + + _amount = _after.sub(_before); + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + + emit Deposit(msg.sender, shares, _amount, getPricePerFullShare()); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + function withdraw(uint256 _shares) public { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + uint256 b = token.balanceOf(address(this)); + + if (b < r) { + uint256 _withdraw = r.sub(b); + IController(controller).withdraw(address(token), _withdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + + token.safeTransfer(msg.sender, r); + emit Withdraw(msg.sender, _shares, r, getPricePerFullShare()); + } + + function getPricePerFullShare() public view returns (uint256) { + return totalSupply() == 0 ? 1e18 : balance().mul(1e18).div(totalSupply()); + } +} diff --git a/avax/contracts/interfaces/IController.sol b/avax/contracts/interfaces/IController.sol new file mode 100644 index 0000000..ea105c8 --- /dev/null +++ b/avax/contracts/interfaces/IController.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} diff --git a/avax/contracts/interfaces/ICurveGauge.sol b/avax/contracts/interfaces/ICurveGauge.sol new file mode 100644 index 0000000..ed52cfd --- /dev/null +++ b/avax/contracts/interfaces/ICurveGauge.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +interface ICurveGauge { + function deposit(uint256) external; + + function balanceOf(address) external view returns (uint256); + + function withdraw(uint256) external; + + function claim_rewards() external; +} diff --git a/avax/contracts/interfaces/ICurvePool.sol b/avax/contracts/interfaces/ICurvePool.sol new file mode 100644 index 0000000..3ca038c --- /dev/null +++ b/avax/contracts/interfaces/ICurvePool.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +interface ICurvePool { + function add_liquidity( + uint256[2] calldata amounts, + uint256 min_mint_amount, + bool use_underlying + ) external returns (uint256); + + function add_liquidity( + uint256[3] calldata amounts, + uint256 min_mint_amount, + bool use_underlying + ) external returns (uint256); + + function add_liquidity( + uint256[4] calldata amounts, + uint256 min_mint_amount, + bool use_underlying + ) external returns (uint256); + + function add_liquidity( + uint256[5] calldata amounts, + uint256 min_mint_amount, + bool use_underlying + ) external returns (uint256); +} diff --git a/avax/contracts/interfaces/IUniswapRouter.sol b/avax/contracts/interfaces/IUniswapRouter.sol new file mode 100644 index 0000000..c8f4b8d --- /dev/null +++ b/avax/contracts/interfaces/IUniswapRouter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2; + +interface IUniswapRouter { + function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); + + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); +} diff --git a/avax/contracts/strategies/StrategyCurveAave.sol b/avax/contracts/strategies/StrategyCurveAave.sol new file mode 100644 index 0000000..67d7a35 --- /dev/null +++ b/avax/contracts/strategies/StrategyCurveAave.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +import "../interfaces/IUniswapRouter.sol"; +import "../interfaces/ICurvePool.sol"; +import "../interfaces/ICurveGauge.sol"; +import "../interfaces/IController.sol"; + +contract StrategyCurveAave { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address(0x1337BedC9D22ecbe766dF105c9623922A27963EC); + + address public constant usdc = address(0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664); + + address public constant crv = address(0x249848BeCA43aC405b8102Ec90Dd5F22CA513c06); // wrong + + address public constant wavax = address(0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7); + + address public constant pool = address(0x7f90122BF0700F9E7e1F688fe926940E8839F353); + + address public gauge = address(0x5B5CFE992AdAC0C9D48E05854B2d91C73a003858); + + address public constant joeRouter = address(0x60aE616a2155Ee3d9A68541Ba4544862310933d4); + + uint256 public performanceFee = 1500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCurveAave"; + } + + function setGauge(address _gauge) external onlyGovernance { + gauge = _gauge; + } + + function setStrategist(address _strategist) external onlyGovernance { + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(gauge, _want); + ICurveGauge(gauge).deposit(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external onlyController returns (uint256 balance) { + require(want != address(_asset), "want"); + require(usdc != address(_asset), "usdc"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal { + ICurveGauge(gauge).withdraw(_amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = balanceOfWant(); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + uint256 _before = balanceOf(); + _withdrawSome(balanceOfPool()); + require(_before == balanceOf(), "!slippage"); + } + + function swapCRVToUsdc(uint256 amount) internal {} + + function swapWMATICToUsdc(uint256 amount) internal {} + + function _swapTraderJoeWithPath(address[] memory path, uint256 _amount) internal returns (uint256) { + IERC20(path[0]).safeApprove(joeRouter, 0); + IERC20(path[0]).safeApprove(joeRouter, _amount); + + uint256[] memory amounts = IUniswapRouter(joeRouter).getAmountsOut(_amount, path); + + uint256 minAmount = amounts[path.length - 1].mul(995).div(1000); + + uint256[] memory outputs = IUniswapRouter(joeRouter).swapExactTokensForTokens( + _amount, + minAmount, + path, + address(this), + now.add(1800) + ); + + return outputs[1]; + } + + function harvest() public { + require(msg.sender == strategist || msg.sender == governance, "!authorized"); + + ICurveGauge(gauge).claim_rewards(); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _wavax = IERC20(wavax).balanceOf(address(this)); + + if (_crv > 0) {} + + if (_wavax > 0) { + address[] memory wavax_usdc_path = new address[](2); + wavax_usdc_path[0] = wavax; + wavax_usdc_path[1] = usdc; + + _swapTraderJoeWithPath(wavax_usdc_path, _wavax); + } + + uint256 _usdc = IERC20(usdc).balanceOf(address(this)); + + if (_usdc > 0) { + IERC20(usdc).approve(pool, _usdc); + ICurvePool(pool).add_liquidity([0, _usdc, 0], 0, true); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return ICurveGauge(gauge).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/avax/contracts/strategies/StrategyCurveAaveNew.sol b/avax/contracts/strategies/StrategyCurveAaveNew.sol new file mode 100644 index 0000000..cdfb292 --- /dev/null +++ b/avax/contracts/strategies/StrategyCurveAaveNew.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +import "../interfaces/IUniswapRouter.sol"; +import "../interfaces/ICurvePool.sol"; +import "../interfaces/ICurveGauge.sol"; +import "../interfaces/IController.sol"; + +contract StrategyCurveAaveNew { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address(0x1337BedC9D22ecbe766dF105c9623922A27963EC); + + address public constant usdc = address(0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664); + + address public constant crv = address(0x47536F17F4fF30e64A96a7555826b8f9e66ec468); // correct + + address public constant wavax = address(0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7); + + address public constant pool = address(0x7f90122BF0700F9E7e1F688fe926940E8839F353); + + address public gauge = address(0x5B5CFE992AdAC0C9D48E05854B2d91C73a003858); + + address public constant joeRouter = address(0x60aE616a2155Ee3d9A68541Ba4544862310933d4); + + uint256 public performanceFee = 1500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCurveAave"; + } + + function setGauge(address _gauge) external onlyGovernance { + gauge = _gauge; + } + + function setStrategist(address _strategist) external onlyGovernance { + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(gauge, _want); + ICurveGauge(gauge).deposit(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external onlyController returns (uint256 balance) { + require(want != address(_asset), "want"); + require(usdc != address(_asset), "usdc"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal { + ICurveGauge(gauge).withdraw(_amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = balanceOfWant(); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + uint256 _before = balanceOf(); + _withdrawSome(balanceOfPool()); + require(_before == balanceOf(), "!slippage"); + } + + function swapCRVToUsdc(uint256 amount) internal {} + + function swapWMATICToUsdc(uint256 amount) internal {} + + function _swapTraderJoeWithPath(address[] memory path, uint256 _amount) internal returns (uint256) { + IERC20(path[0]).safeApprove(joeRouter, 0); + IERC20(path[0]).safeApprove(joeRouter, _amount); + + uint256[] memory amounts = IUniswapRouter(joeRouter).getAmountsOut(_amount, path); + + uint256 minAmount = amounts[path.length - 1].mul(995).div(1000); + + uint256[] memory outputs = IUniswapRouter(joeRouter).swapExactTokensForTokens( + _amount, + minAmount, + path, + address(this), + now.add(1800) + ); + + return outputs[1]; + } + + function harvest() public { + require(msg.sender == strategist || msg.sender == governance, "!authorized"); + + ICurveGauge(gauge).claim_rewards(); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _wavax = IERC20(wavax).balanceOf(address(this)); + + if (_crv > 0) { + address[] memory crv_usdc_path = new address[](2); + crv_usdc_path[0] = crv; + crv_usdc_path[1] = wavax; + + _swapTraderJoeWithPath(crv_usdc_path, _crv); + } + + if (_wavax > 0) { + address[] memory wavax_usdc_path = new address[](2); + wavax_usdc_path[0] = wavax; + wavax_usdc_path[1] = usdc; + + _swapTraderJoeWithPath(wavax_usdc_path, _wavax); + } + + uint256 _usdc = IERC20(usdc).balanceOf(address(this)); + + if (_usdc > 0) { + IERC20(usdc).approve(pool, _usdc); + ICurvePool(pool).add_liquidity([0, _usdc, 0], 0, true); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return ICurveGauge(gauge).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/avax/hardhat.config.ts b/avax/hardhat.config.ts new file mode 100644 index 0000000..cb372d1 --- /dev/null +++ b/avax/hardhat.config.ts @@ -0,0 +1,46 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomiclabs/hardhat-waffle"; +import "@nomiclabs/hardhat-etherscan"; +import "@nomiclabs/hardhat-vyper"; + +import "hardhat-deploy"; +import "hardhat-deploy-ethers"; +import "./tasks/global"; + +require("dotenv").config(); + +const TESTING_DEPLOYER_PKEY = process.env.TESTING_DEPLOYER_PKEY; + +export default { + defaultNetwork: "hardhat", + networks: { + hardhat: { + forking: { + url: `https://api.avax.network/ext/bc/C/rpc`, + }, + }, + avax: { + url: `https://api.avax.network/ext/bc/C/rpc`, + accounts: [`0x${TESTING_DEPLOYER_PKEY}`], + gasPrice: 225000000000, + }, + }, + namedAccounts: { + deployer: 0, + }, + vyper: { + version: "0.2.7", + }, + solidity: { + compilers: [{ version: "0.8.0" }, { version: "0.7.4" }, { version: "0.6.12" }, { version: "0.5.17" }], + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + etherscan: { + apiKey: process.env.ETHERSCAN_KEY, + }, +} as HardhatUserConfig; diff --git a/avax/package.json b/avax/package.json new file mode 100644 index 0000000..7ec33de --- /dev/null +++ b/avax/package.json @@ -0,0 +1,35 @@ +{ + "name": "sd-avax", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "@nomiclabs/hardhat-etherscan": "2.1.3", + "@nomiclabs/hardhat-waffle": "^2.0.0", + "@types/chai": "^4.2.17", + "@types/mocha": "^8.2.2", + "@types/node": "^15.0.1", + "chai": "^4.3.4", + "ethereum-waffle": "^3.0.0", + "ethers": "^5.4.0", + "hardhat": "^2.6.0", + "hardhat-abi-exporter": "^2.2.1", + "prettier-plugin-solidity": "^1.0.0-beta.10", + "ts-node": "^9.1.1", + "typescript": "^4.2.4" + }, + "dependencies": { + "@0xsequence/erc-1155": "^3.0.4", + "@chainlink/contracts": "^0.1.7", + "@ethersproject/units": "^5.3.0", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-vyper": "^2.0.1", + "@openzeppelin/contracts": "3.4.0", + "aigle": "^1.14.1", + "dotenv": "^8.2.0", + "hardhat-deploy": "^0.8.11", + "hardhat-deploy-ethers": "^0.3.0-beta.10", + "prettier": "^2.3.0" + }, + "scripts": {} +} diff --git a/avax/readme.md b/avax/readme.md new file mode 100644 index 0000000..f07c2a6 --- /dev/null +++ b/avax/readme.md @@ -0,0 +1,12 @@ +## Deployment steps + +```sh +npx hardhat deploy --network avax --export deployment.json +npx hardhat sourcify --network avax +npx hardhat configure-aave --network avax +``` + +Multirewards contract are taken from Curve +[MultiRewards](https://github.com/curvefi/multi-rewards/blob/master/contracts/MultiRewards.sol) +And differences can be inspected on the [Diffchecker](https://www.diffchecker.com/JnKF55JE) + diff --git a/avax/test/aave.ts b/avax/test/aave.ts new file mode 100644 index 0000000..a5a74d2 --- /dev/null +++ b/avax/test/aave.ts @@ -0,0 +1,102 @@ +import { ethers, network } from "hardhat"; +import { expect } from "chai"; +import { BigNumber } from "@ethersproject/bignumber"; +import { Contract } from "@ethersproject/contracts"; +import { JsonRpcSigner } from "@ethersproject/providers"; + +import ERC20ABI from "./fixtures/ERC20.json"; + +const WANT = "0x1337BedC9D22ecbe766dF105c9623922A27963EC"; +const WANT_HOLDER = "0x5ede2CBA38aE20EAc6dF5595651e4f8E5084940B"; + +describe("Aave strat", function () { + let controller: Contract; + let vault: Contract; + let strategy: Contract; + let want: Contract; + let holderSigner: JsonRpcSigner; + let initialHolderBalance: BigNumber; + + before(async function () { + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [WANT_HOLDER], + }); + + const Controller = await ethers.getContractFactory("Controller"); + const Vault = await ethers.getContractFactory("Vault"); + const Strategy = await ethers.getContractFactory("StrategyCurveAave"); + + controller = await Controller.deploy(owner.address); + vault = await Vault.deploy(WANT, controller.address, owner.address, "StakeDAO", "sd"); + strategy = await Strategy.deploy(controller.address); + + want = await ethers.getContractAt(ERC20ABI, WANT); + holderSigner = await ethers.provider.getSigner(WANT_HOLDER); + + await (await controller.approveStrategy(WANT, strategy.address)).wait(); + await (await controller.setStrategy(WANT, strategy.address)).wait(); + await (await controller.setVault(WANT, vault.address)).wait(); + + // approve + initialHolderBalance = await want.balanceOf(WANT_HOLDER); + await (await want.connect(holderSigner).approve(vault.address, initialHolderBalance)).wait(); + }); + + it("Should deposit in vault", async function () { + const holderBalance = await want.balanceOf(WANT_HOLDER); + await (await vault.connect(holderSigner).deposit(holderBalance)).wait(); + const vaultBalance = await want.balanceOf(vault.address); + expect(vaultBalance).to.be.equal(holderBalance); + }); + + it("Should deposit in strategy", async function () { + const vaultBalance = await want.balanceOf(vault.address); + await (await vault.earn()).wait(); + const strategyBalance = await strategy.balanceOfPool(); + expect(vaultBalance).to.be.equal(strategyBalance); + }); + + it("Should harvest", async function () { + await network.provider.send("evm_increaseTime", [3600]); + await network.provider.send("evm_mine"); + + const strategyBalanceBefore = await strategy.balanceOf(); + await (await strategy.harvest()).wait(); + const strategyBalanceAfter = await strategy.balanceOf(); + expect(strategyBalanceAfter).to.be.gt(strategyBalanceBefore); + }); + + it("Should withdraw all", async function () { + const holderBalanceBefore = await want.balanceOf(WANT_HOLDER); + const holderSDBalanceBefore = await vault.balanceOf(WANT_HOLDER); + const withdrawAmount = holderSDBalanceBefore; + + const pricePerShare = await vault.getPricePerFullShare(); + + await (await vault.connect(holderSigner).withdraw(withdrawAmount)).wait(); + + const holderBalanceAfter = await want.balanceOf(WANT_HOLDER); + const holderSDBalanceAfter = await vault.balanceOf(WANT_HOLDER); + + const withdrawInWant = withdrawAmount.mul(pricePerShare).div(BigNumber.from(10).pow(18)); + const fees = withdrawInWant.mul(50).div(10000); + + const expected = holderBalanceBefore.add(withdrawInWant).sub(fees); + + console.log({ + expected: expected.toString(), + holderBalanceAfter: holderBalanceAfter.toString(), + }); + + expect(holderBalanceAfter).to.be.equal(expected); + + // correct SD amount has be withdrawn + expect(holderSDBalanceBefore.sub(withdrawAmount)).to.be.equal(holderSDBalanceAfter); + + // you won some (even with 0.5% withdrawal fees) + expect(holderBalanceAfter).to.be.gt(initialHolderBalance.mul(995).div(1000)); + }); +}); diff --git a/avax/test/fixtures/ERC20.json b/avax/test/fixtures/ERC20.json new file mode 100644 index 0000000..d7661a4 --- /dev/null +++ b/avax/test/fixtures/ERC20.json @@ -0,0 +1,152 @@ +[ + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "subtractedValue", "type": "uint256" } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "addedValue", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_to", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/avax/test/gauge.ts b/avax/test/gauge.ts new file mode 100644 index 0000000..1cbb912 --- /dev/null +++ b/avax/test/gauge.ts @@ -0,0 +1,115 @@ +import { ethers, network } from "hardhat"; +import { expect } from "chai"; +import { BigNumber } from "@ethersproject/bignumber"; +import { Contract } from "@ethersproject/contracts"; +import { JsonRpcSigner } from "@ethersproject/providers"; + +import ERC20ABI from "./fixtures/ERC20.json"; +import { parseEther } from "@ethersproject/units"; + +const WANT = "0x0665eF3556520B21368754Fb644eD3ebF1993AD4"; // sdav3CRV +const WANT_HOLDER = "0xe582c162c9C06F48b8286cbFD7e0257FB1d47ec2"; // sdav3CRV holder +const WAVAX = "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"; +const WAVAX_HOLDER = "0x331d3D8f0c4618B22e1BBe0c262e09d46821a441"; +const DEPLOYER = "0xb36a0671B3D49587236d7833B01E79798175875f"; // sd deployer +const SDT = "0x8323FCF06D58F49533Da60906D731c6a56626Fb2"; +const SDT_HOLDER = "0x0411e650dff0c6680c9e147f2413e0480718aa27"; + +describe("Gauge multi rewards", function () { + let vault: Contract; + let gauge: Contract; + let sdav3CRV: Contract; + let wavax: Contract; + let sdt: Contract; + let holderSigner: JsonRpcSigner; + let deployerSigner: JsonRpcSigner; + let wavaxHolder: JsonRpcSigner; + let sdtHolder: JsonRpcSigner; + + before(async function () { + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [WANT_HOLDER], + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [DEPLOYER], + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [WAVAX_HOLDER], + }); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [SDT_HOLDER], + }); + + holderSigner = await ethers.provider.getSigner(WANT_HOLDER); + deployerSigner = await ethers.provider.getSigner(DEPLOYER); + wavaxHolder = await ethers.provider.getSigner(WAVAX_HOLDER); + sdtHolder = await ethers.provider.getSigner(SDT_HOLDER); + + const Gauge = await ethers.getContractFactory("MultiRewards"); + + sdav3CRV = await ethers.getContractAt(ERC20ABI, WANT); + wavax = await ethers.getContractAt(ERC20ABI, WAVAX); + sdt = await ethers.getContractAt(ERC20ABI, SDT); + + await wavax.connect(wavaxHolder).transfer(deployerSigner._address, parseEther("1000")) + await sdt.connect(sdtHolder).transfer(deployerSigner._address, parseEther("10")) + + gauge = await Gauge.connect(deployerSigner).deploy(sdav3CRV.address); + await gauge.addReward(WAVAX, deployerSigner._address, 86400 * 3); // 3 days of reward + + }); + + it("Should notify WAVAX reward", async function () { + const amount = parseEther("1000") + await wavax.connect(deployerSigner).approve(gauge.address, amount) + await gauge.connect(deployerSigner).notifyRewardAmount(WAVAX, amount) + const gaugeBalance = await wavax.balanceOf(gauge.address) + expect(gaugeBalance).to.be.eq(amount) + }); + + it("Should deposit into the gauge", async function () { + const amount = parseEther("10") + await sdav3CRV.connect(holderSigner).approve(gauge.address, amount) + await gauge.connect(holderSigner).stake(amount); + const gaugeBalance = await sdav3CRV.balanceOf(gauge.address); + expect(gaugeBalance).to.be.eq(amount); + }); + + it("Should get reward in AVAX", async function () { + await network.provider.send("evm_increaseTime", [86400 * 2]); // 1 day + await network.provider.send("evm_mine", []); + await gauge.connect(holderSigner).getReward() + const balance = await wavax.balanceOf(holderSigner._address) + expect(balance).to.be.gt(0) + }); + + it("Add and notify reward in SDT", async function () { + const sdtAmount = parseEther("10") + await gauge.addReward(SDT, deployerSigner._address, 86400 * 3) + await sdt.connect(deployerSigner).approve(gauge.address, sdtAmount) + await gauge.notifyRewardAmount(SDT, sdtAmount) + }); + + it("Should get reward in SDT", async function () { + await network.provider.send("evm_increaseTime", [86400 * 2]); // 2 days + await network.provider.send("evm_mine", []); + await gauge.connect(holderSigner).getReward() + const balance = await sdt.balanceOf(holderSigner._address) + expect(balance).to.be.gt(0) + }); + + it("Should widraw from gauge", async function () { + await gauge.connect(holderSigner).withdraw(parseEther("10")) + const gaugeBalance = await sdav3CRV.balanceOf(gauge.address) + expect(gaugeBalance).to.be.eq(0) + }); +}); diff --git a/avax/test/strategyMigration.ts b/avax/test/strategyMigration.ts new file mode 100644 index 0000000..0fdf3cf --- /dev/null +++ b/avax/test/strategyMigration.ts @@ -0,0 +1,76 @@ +import { ethers, network } from "hardhat"; +import { expect } from "chai"; +import { BigNumber } from "@ethersproject/bignumber"; +import { Contract } from "@ethersproject/contracts"; +import { JsonRpcSigner } from "@ethersproject/providers"; + +import ERC20ABI from "./fixtures/ERC20.json"; + +const CONTROLLER = "0x01f0ea8d52247428A7CEF21327f78d7E00d37502"; +const CONTROLLER_ADMIN = "0xb36a0671b3d49587236d7833b01e79798175875f"; +const AAVE_STRATEGY = "0x22D0b68a88BCFf9A0D9F08fFf03dd969Eed094Ca"; +const VAULT = "0x0665eF3556520B21368754Fb644eD3ebF1993AD4"; +const CRV = "0x47536F17F4fF30e64A96a7555826b8f9e66ec468"; + +describe("Aave strategy migration", function () { + let controller: Contract; + let vault: Contract; + let strategy: Contract; + let strategyNew: Contract; + let crv: Contract; + let want: Contract; + let controllerAdmmin: JsonRpcSigner; + + before(async function () { + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [CONTROLLER_ADMIN] + }); + + const StrategyNew = await ethers.getContractFactory("StrategyCurveAaveNew"); + controllerAdmmin = await ethers.provider.getSigner(CONTROLLER_ADMIN); + controller = await ethers.getContractAt("Controller", CONTROLLER); + vault = await ethers.getContractAt("Vault", VAULT); + strategyNew = await StrategyNew.deploy(CONTROLLER); + strategy = await ethers.getContractAt("StrategyCurveAave", AAVE_STRATEGY); + crv = await ethers.getContractAt(ERC20ABI, CRV); + want = await ethers.getContractAt(ERC20ABI, vault.token()); + }); + + it("Should withdraw all CRV in stuck before the migration", async function () { + const strategyCrvBalance = await crv.balanceOf(strategy.address); + await controller.connect(controllerAdmmin).inCaseStrategyTokenGetStuck(strategy.address, crv.address); + const controllerCrvBalance = await crv.balanceOf(controller.address); + expect(controllerCrvBalance).to.be.eq(strategyCrvBalance); + const strategyCrvBalanceAfter = await crv.balanceOf(strategy.address); + expect(strategyCrvBalanceAfter).to.be.eq(0); + }); + + it("Should set the new strategy via controller and call earn in vault", async function () { + this.enableTimeouts(false); + const strategyWantBefore = await strategy.balanceOfPool(); + await strategy.connect(controllerAdmmin).harvest(); + const strategyWantAfter = await strategy.balanceOfPool(); + expect(strategyWantAfter).to.be.gt(strategyWantBefore); + await controller.connect(controllerAdmmin).approveStrategy(want.address, strategyNew.address); + const vaultWantBefore = await want.balanceOf(vault.address); + await controller.connect(controllerAdmmin).setStrategy(want.address, strategyNew.address); + const vaultWantAfter = await want.balanceOf(vault.address); + const wantReceived = vaultWantAfter.sub(vaultWantBefore); + expect(strategyWantAfter).to.be.eq(wantReceived); + await vault.earn(); + const newStrategyWant = await strategyNew.balanceOfPool(); + expect(newStrategyWant).to.be.eq(vaultWantAfter); + }); + + it("Should call harvest in strategy", async function () { + const strategyBefore = await strategyNew.balanceOfPool(); + await network.provider.send("evm_increaseTime", [86400 * 2]); // 2 days + await network.provider.send("evm_mine", []); + await strategyNew.harvest(); + const strategyAfter = await strategyNew.balanceOfPool(); + expect(strategyAfter).to.be.gt(strategyBefore); + }); +}); diff --git a/avax/tsconfig.json b/avax/tsconfig.json new file mode 100644 index 0000000..7fe7de9 --- /dev/null +++ b/avax/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "resolveJsonModule": true + }, + "include": ["./scripts", "./test"], + "files": ["./hardhat.config.ts"] +} diff --git a/convex/.gitignore b/convex/.gitignore new file mode 100644 index 0000000..8413c26 --- /dev/null +++ b/convex/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +artifacts +cache +.env +.vscode \ No newline at end of file diff --git a/convex/abis/BaseRewardPool.json b/convex/abis/BaseRewardPool.json new file mode 100644 index 0000000..ffc42dc --- /dev/null +++ b/convex/abis/BaseRewardPool.json @@ -0,0 +1,363 @@ +[ + { + "inputs": [ + {"internalType": "uint256", "name": "pid_", "type": "uint256"}, + {"internalType": "address", "name": "stakingToken_", "type": "address"}, + {"internalType": "address", "name": "rewardToken_", "type": "address"}, + {"internalType": "address", "name": "operator_", "type": "address"}, + {"internalType": "address", "name": "rewardManager_", "type": "address"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "RewardAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "RewardPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [ + {"internalType": "address", "name": "_reward", "type": "address"} + ], + "name": "addExtraReward", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "clearExtraRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "currentRewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "donate", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "duration", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "earned", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "name": "extraRewards", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "extraRewardsLength", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getReward", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "_account", "type": "address"}, + {"internalType": "bool", "name": "_claimExtras", "type": "bool"} + ], + "name": "getReward", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "historicalRewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastTimeRewardApplicable", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastUpdateTime", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "newRewardRatio", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "operator", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "periodFinish", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pid", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "_rewards", "type": "uint256"} + ], + "name": "queueNewRewards", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "queuedRewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardManager", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerToken", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerTokenStored", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardRate", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardToken", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "rewards", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "stake", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stakeAll", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "_for", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "stakeFor", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stakingToken", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "userRewardPerTokenPaid", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "amount", "type": "uint256"}, + {"internalType": "bool", "name": "claim", "type": "bool"} + ], + "name": "withdraw", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "bool", "name": "claim", "type": "bool"}], + "name": "withdrawAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "bool", "name": "claim", "type": "bool"}], + "name": "withdrawAllAndUnwrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "amount", "type": "uint256"}, + {"internalType": "bool", "name": "claim", "type": "bool"} + ], + "name": "withdrawAndUnwrap", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/convex/abis/Controller.json b/convex/abis/Controller.json new file mode 100644 index 0000000..5241e1d --- /dev/null +++ b/convex/abis/Controller.json @@ -0,0 +1,320 @@ +[ + { + "inputs": [ + {"internalType": "address", "name": "_rewards", "type": "address"} + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "approveStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "approvedStrategies", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "converters", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "earn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "parts", "type": "uint256"} + ], + "name": "getExpectedReturn", + "outputs": [ + {"internalType": "uint256", "name": "expected", "type": "uint256"} + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "governance", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "inCaseStrategyTokenGetStuck", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "inCaseTokensGetStuck", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "max", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "onesplit", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "revokeStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "rewards", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_input", "type": "address"}, + {"internalType": "address", "name": "_output", "type": "address"}, + {"internalType": "address", "name": "_converter", "type": "address"} + ], + "name": "setConverter", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_governance", "type": "address"} + ], + "name": "setGovernance", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_onesplit", "type": "address"} + ], + "name": "setOneSplit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_rewards", "type": "address"} + ], + "name": "setRewards", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_split", "type": "uint256"} + ], + "name": "setSplit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategist", "type": "address"} + ], + "name": "setStrategist", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_strategy", "type": "address"} + ], + "name": "setStrategy", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_vault", "type": "address"} + ], + "name": "setVault", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "split", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "strategies", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "strategist", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "vaults", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"} + ], + "name": "withdrawAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_strategy", "type": "address"}, + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "uint256", "name": "parts", "type": "uint256"} + ], + "name": "yearn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/convex/abis/ERC20.json b/convex/abis/ERC20.json new file mode 100644 index 0000000..f43b285 --- /dev/null +++ b/convex/abis/ERC20.json @@ -0,0 +1,159 @@ +[ + { + "name": "Transfer", + "inputs": [ + {"type": "address", "name": "_from", "indexed": true}, + {"type": "address", "name": "_to", "indexed": true}, + {"type": "uint256", "name": "_value", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "Approval", + "inputs": [ + {"type": "address", "name": "_owner", "indexed": true}, + {"type": "address", "name": "_spender", "indexed": true}, + {"type": "uint256", "name": "_value", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "outputs": [], + "inputs": [ + {"type": "string", "name": "_name"}, + {"type": "string", "name": "_symbol"}, + {"type": "uint256", "name": "_decimals"}, + {"type": "uint256", "name": "_supply"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "name": "set_minter", + "outputs": [], + "inputs": [{"type": "address", "name": "_minter"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 36247 + }, + { + "name": "set_name", + "outputs": [], + "inputs": [ + {"type": "string", "name": "_name"}, + {"type": "string", "name": "_symbol"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 178069 + }, + { + "name": "totalSupply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1211 + }, + { + "name": "allowance", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [ + {"type": "address", "name": "_owner"}, + {"type": "address", "name": "_spender"} + ], + "stateMutability": "view", + "type": "function", + "gas": 1549 + }, + { + "name": "transfer", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 74832 + }, + { + "name": "transferFrom", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_from"}, + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 111983 + }, + { + "name": "approve", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_spender"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 39078 + }, + { + "name": "mint", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 75808 + }, + { + "name": "burnFrom", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 75826 + }, + { + "name": "name", + "outputs": [{"type": "string", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 7823 + }, + { + "name": "symbol", + "outputs": [{"type": "string", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 6876 + }, + { + "name": "decimals", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1481 + }, + { + "name": "balanceOf", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1665 + } +] diff --git a/convex/abis/YVault.json b/convex/abis/YVault.json new file mode 100644 index 0000000..6ba3c86 --- /dev/null +++ b/convex/abis/YVault.json @@ -0,0 +1,347 @@ +[ + { + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_controller", "type": "address"} + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "owner", "type": "address"}, + {"internalType": "address", "name": "spender", "type": "address"} + ], + "name": "allowance", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "approve", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "available", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "balance", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "controller", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "subtractedValue", "type": "uint256"} + ], + "name": "decreaseAllowance", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "deposit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "depositAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "earn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPricePerFullShare", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "governance", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "reserve", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "harvest", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "addedValue", "type": "uint256"} + ], + "name": "increaseAllowance", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "max", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "min", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_controller", "type": "address"} + ], + "name": "setController", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_governance", "type": "address"} + ], + "name": "setGovernance", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{"internalType": "uint256", "name": "_min", "type": "uint256"}], + "name": "setMin", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "recipient", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transfer", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "sender", "type": "address"}, + {"internalType": "address", "name": "recipient", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transferFrom", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_shares", "type": "uint256"} + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "withdrawAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/convex/contracts/Strategy3CrvConvex.sol b/convex/contracts/Strategy3CrvConvex.sol new file mode 100644 index 0000000..8b41d01 --- /dev/null +++ b/convex/contracts/Strategy3CrvConvex.sol @@ -0,0 +1,350 @@ +pragma solidity ^0.5.17; + +// yarn add @openzeppelin/contracts@2.5.1 +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import 'hardhat/console.sol'; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[3] calldata, uint256) external; + + function calc_token_amount(uint256[3] calldata, bool) + external + returns (uint256); +} + +contract Strategy3CrvConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // 3crv + address public constant want = + address(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant usdc = + address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public constant voter = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public constant sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public constant curve = + address(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); + + uint256 public keepCRV = 0; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, '!governance'); + _; + } + + modifier onlyController() { + require(msg.sender == controller, '!controller'); + _; + } + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0x689440f2Ff927E1f24c72F1087E1FAF471eCe1c8); + } + + function getName() external pure returns (string memory) { + return 'Strategy3CrvConvex'; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + '!authorized' + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(9, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), 'want'); + require(cvx != address(_asset), 'cvx'); + require(crv != address(_asset), 'crv'); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + // slippageCRV = 100 for 1% max slippage + function harvest( + uint256 maxSlippageCRV, + uint256 maxSlippageCVX, + uint256 maxSlippageCRVAddLiquidity + ) public { + require( + msg.sender == strategist || msg.sender == governance, + '!authorized' + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = usdc; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCRV).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](3); + path[0] = cvx; + path[1] = weth; + path[2] = usdc; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCVX).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + uint256 _usdc = IERC20(usdc).balanceOf(address(this)); + + if (_usdc > 0) { + IERC20(usdc).safeApprove(curve, 0); + IERC20(usdc).safeApprove(curve, _usdc); + + uint256 _tokenAmount = + ICurveFi(curve).calc_token_amount([0, _usdc, 0], true); + + uint256 _minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + ICurveFi(curve).add_liquidity([0, _usdc, 0], _minimalAmount); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/convex/contracts/StrategyEursConvex.sol b/convex/contracts/StrategyEursConvex.sol new file mode 100644 index 0000000..9f8e754 --- /dev/null +++ b/convex/contracts/StrategyEursConvex.sol @@ -0,0 +1,416 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +// yarn add @openzeppelin/contracts@2.5.1 +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +import 'hardhat/console.sol'; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[2] calldata, uint256) external; + + function calc_token_amount(uint256[2] calldata, bool) + external + returns (uint256); +} + +interface ISwapRouter { + function uniswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external; + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + function exactInput(ExactInputParams calldata params) + external + returns (uint256 amountOut); + + function quoteExactInput(bytes calldata path, uint256 amountIn) + external + returns (uint256 amountOut); +} + +contract StrategyEursConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // eursCRV + address public constant want = + address(0x194eBd173F6cDacE046C53eACcE9B953F28411d1); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant eurs = + address(0xdB25f211AB05b1c97D595516F45794528a807ad8); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public constant usdc = + address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + address public constant voter = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public constant sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public constant curve = + address(0x0Ce6a5fF5217e38315f87032CF90686C96627CAA); + + address public constant quoter = + address(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6); + + address public constant uniswapRouterAddress = + address(0xE592427A0AEce92De3Edee1F18E0157C05861564); + + ISwapRouter public constant uniswapRouter = ISwapRouter(uniswapRouterAddress); + + uint256 public keepCRV = 0; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, '!governance'); + _; + } + + modifier onlyController() { + require(msg.sender == controller, '!controller'); + _; + } + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0xcB8F69E0064d8cdD29cbEb45A14cf771D904BcD3); + } + + function getName() external pure returns (string memory) { + return 'StrategyEursConvex'; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + '!authorized' + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(22, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), 'want'); + require(cvx != address(_asset), 'cvx'); + require(crv != address(_asset), 'crv'); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + function swapToEurs(uint256 maxSlippageEURS) internal { + uint256 _weth = IERC20(weth).balanceOf(address(this)); + + IERC20(weth).safeApprove(uniswapRouterAddress, 0); + IERC20(weth).safeApprove(uniswapRouterAddress, _weth); + + uint256 amountOut = + ISwapRouter(quoter).quoteExactInput( + abi.encodePacked(weth, uint24(3000), usdc, uint24(500), eurs), + _weth + ); + + uint256 minAmountOut = amountOut.mul(10000 - maxSlippageEURS).div(10000); + + ISwapRouter.ExactInputParams memory params = + ISwapRouter.ExactInputParams( + abi.encodePacked(weth, uint24(3000), usdc, uint24(500), eurs), + address(this), + now.add(1800), + _weth, + minAmountOut + ); + + uniswapRouter.exactInput(params); + } + + function harvest( + uint256 maxSlippageCRV, + uint256 maxSlippageCVX, + uint256 maxSlippageCRVAddLiquidity, + uint256 maxSlippageEURS + ) public { + require( + msg.sender == strategist || msg.sender == governance, + '!authorized' + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](2); + path[0] = crv; + path[1] = weth; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = + _amounts[1].mul(10000 - maxSlippageCRV).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](2); + path[0] = cvx; + path[1] = weth; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = + _amounts[1].mul(10000 - maxSlippageCVX).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + uint256 _weth = IERC20(weth).balanceOf(address(this)); + + if (_weth > 0) { + swapToEurs(maxSlippageEURS); + } + + uint256 _eurs = IERC20(eurs).balanceOf(address(this)); + + if (_eurs > 0) { + IERC20(eurs).safeApprove(curve, 0); + IERC20(eurs).safeApprove(curve, _eurs); + + uint256 _tokenAmount = + ICurveFi(curve).calc_token_amount([_eurs, 0], true); + uint256 _minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + + ICurveFi(curve).add_liquidity([_eurs, 0], _minimalAmount); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/convex/contracts/StrategyEurtConvex.sol b/convex/contracts/StrategyEurtConvex.sol new file mode 100644 index 0000000..1086c0d --- /dev/null +++ b/convex/contracts/StrategyEurtConvex.sol @@ -0,0 +1,416 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +// yarn add @openzeppelin/contracts@2.5.1 +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +import 'hardhat/console.sol'; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[2] calldata, uint256) external; + + function calc_token_amount(uint256[2] calldata, bool) + external + returns (uint256); +} + +interface ISwapRouter { + function uniswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external; + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + function exactInput(ExactInputParams calldata params) + external + returns (uint256 amountOut); + + function quoteExactInput(bytes calldata path, uint256 amountIn) + external + returns (uint256 amountOut); +} + +contract StrategyEurtConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // EURt-f + address public constant want = + address(0xFD5dB7463a3aB53fD211b4af195c5BCCC1A03890); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant eurt = + address(0xC581b735A1688071A1746c968e0798D642EDE491); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public constant usdc = + address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + address public constant voter = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public constant sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public constant curve = + address(0xFD5dB7463a3aB53fD211b4af195c5BCCC1A03890); + + address public constant quoter = + address(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6); + + address public constant uniswapRouterAddress = + address(0xE592427A0AEce92De3Edee1F18E0157C05861564); + + ISwapRouter public constant uniswapRouter = ISwapRouter(uniswapRouterAddress); + + uint256 public keepCRV = 10; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, '!governance'); + _; + } + + modifier onlyController() { + require(msg.sender == controller, '!controller'); + _; + } + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0xD814BFC091111E1417a669672144aFFAA081c3CE); + } + + function getName() external pure returns (string memory) { + return 'StrategyEurtConvex'; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + '!authorized' + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(39, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), 'want'); + require(cvx != address(_asset), 'cvx'); + require(crv != address(_asset), 'crv'); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + function swapToEurt(uint256 maxSlippageEURT) internal { + uint256 _weth = IERC20(weth).balanceOf(address(this)); + + IERC20(weth).safeApprove(uniswapRouterAddress, 0); + IERC20(weth).safeApprove(uniswapRouterAddress, _weth); + + uint256 amountOut = + ISwapRouter(quoter).quoteExactInput( + abi.encodePacked(weth, uint24(3000), eurt), + _weth + ); + + uint256 minAmountOut = amountOut.mul(10000 - maxSlippageEURT).div(10000); + + ISwapRouter.ExactInputParams memory params = + ISwapRouter.ExactInputParams( + abi.encodePacked(weth, uint24(3000), eurt), + address(this), + now.add(1800), + _weth, + minAmountOut + ); + + uniswapRouter.exactInput(params); + } + + function harvest( + uint256 maxSlippageCRV, + uint256 maxSlippageCVX, + uint256 maxSlippageCRVAddLiquidity, + uint256 maxSlippageEURT + ) public { + require( + msg.sender == strategist || msg.sender == governance, + '!authorized' + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](2); + path[0] = crv; + path[1] = weth; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = + _amounts[1].mul(10000 - maxSlippageCRV).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](2); + path[0] = cvx; + path[1] = weth; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = + _amounts[1].mul(10000 - maxSlippageCVX).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + uint256 _weth = IERC20(weth).balanceOf(address(this)); + + if (_weth > 0) { + swapToEurt(maxSlippageEURT); + } + + uint256 _eurt = IERC20(eurt).balanceOf(address(this)); + + if (_eurt > 0) { + IERC20(eurt).safeApprove(curve, 0); + IERC20(eurt).safeApprove(curve, _eurt); + + uint256 _tokenAmount = + ICurveFi(curve).calc_token_amount([_eurt, 0], true); + uint256 _minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + + ICurveFi(curve).add_liquidity([_eurt, 0], _minimalAmount); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/convex/contracts/StrategyFrxConvex.sol b/convex/contracts/StrategyFrxConvex.sol new file mode 100644 index 0000000..54b4a2a --- /dev/null +++ b/convex/contracts/StrategyFrxConvex.sol @@ -0,0 +1,416 @@ +pragma solidity ^0.5.17; + +// yarn add @openzeppelin/contracts@2.5.1 +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[3] calldata, uint256) external; + + function calc_token_amount(uint256[3] calldata, bool) + external + returns (uint256); +} + +interface IMetapool { + function add_liquidity(uint256[2] calldata, uint256) external; + + function calc_token_amount(uint256[2] calldata, bool) + external + returns (uint256); +} + +contract StrategyFrxConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // Frax3crv + address public constant want = + address(0xd632f22692FaC7611d2AA1C0D552930D43CAEd3B); + + address public constant three_crv = + address(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + + address public constant frx = + address(0x853d955aCEf822Db058eb8505911ED77F175b99e); + + address public constant fxs = + address(0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant usdc = + address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public constant voter = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public constant sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public constant uniRouter = + address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + + address public constant metapool = + address(0xd632f22692FaC7611d2AA1C0D552930D43CAEd3B); + + address public constant three_pool = + address(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); + + uint256 public keepCRV = 10000; + uint256 public performanceFee = 0; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, '!governance'); + _; + } + + modifier onlyController() { + require(msg.sender == controller, '!controller'); + _; + } + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0xB900EF131301B307dB5eFcbed9DBb50A3e209B2e); + } + + function getName() external pure returns (string memory) { + return 'StrategyFrxConvex'; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + '!authorized' + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(32, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), 'want'); + require(cvx != address(_asset), 'cvx'); + require(crv != address(_asset), 'crv'); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + // slippageCRV = 100 for 1% max slippage + function harvest( + uint256 maxSlippageCVX, + uint256 maxSlippageCRV, + uint256 maxSlippageFXS, + uint256 maxSlippageCRVAddLiquidity + ) public { + require( + msg.sender == strategist || msg.sender == governance, + '!authorized' + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + uint256 _fxs = IERC20(fxs).balanceOf(address(this)); + + // sending keepCRV to voter and swap the remaining + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + if (_crv > 0) { + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = usdc; + + uint256[] memory _amounts = + Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCRV).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + } + + // swapping fxs to frx on UniV2 + if (_fxs > 0) { + IERC20(fxs).safeApprove(uniRouter, 0); + IERC20(fxs).safeApprove(uniRouter, _fxs); + + address[] memory path = new address[](2); + path[0] = fxs; + path[1] = frx; + + uint256[] memory _amounts = Sushi(uniRouter).getAmountsOut(_fxs, path); + uint256 _minimalAmount = + _amounts[1].mul(10000 - maxSlippageFXS).div(10000); + + Sushi(uniRouter).swapExactTokensForTokens( + _fxs, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + uint256 _frx = IERC20(frx).balanceOf(address(this)); + + // swapping cvx to usdc on Sushi + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](3); + path[0] = cvx; + path[1] = weth; + path[2] = usdc; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCVX).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + uint256 _usdc = IERC20(usdc).balanceOf(address(this)); + + // add_liquidity'ing usdc to 3pool, to get 3CRV + if (_usdc > 0) { + IERC20(usdc).safeApprove(three_pool, 0); + IERC20(usdc).safeApprove(three_pool, _usdc); + + uint256 _tokenAmount = + ICurveFi(three_pool).calc_token_amount([0, _usdc, 0], true); + + uint256 __minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + ICurveFi(three_pool).add_liquidity([0, _usdc, 0], __minimalAmount); + } + } + uint256 _three_crv = IERC20(three_crv).balanceOf(address(this)); + + // add_liquidity'ing frx and/or 3CRV to frax metapool for want + if (_frx > 0 || _three_crv > 0) { + IERC20(frx).safeApprove(metapool, 0); + IERC20(frx).safeApprove(metapool, _frx); + IERC20(three_crv).safeApprove(metapool, 0); + IERC20(three_crv).safeApprove(metapool, _three_crv); + + uint256 _tokenAmount = + IMetapool(metapool).calc_token_amount([_frx, _three_crv], true); + + uint256 _minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + IMetapool(metapool).add_liquidity([_frx, _three_crv], _minimalAmount); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/convex/contracts/StrategySEthConvex.sol b/convex/contracts/StrategySEthConvex.sol new file mode 100644 index 0000000..25329eb --- /dev/null +++ b/convex/contracts/StrategySEthConvex.sol @@ -0,0 +1,357 @@ +pragma solidity ^0.5.17; + +// yarn add @openzeppelin/contracts@2.5.1 +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import 'hardhat/console.sol'; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[2] calldata, uint256) payable external; + + function calc_token_amount(uint256[2] calldata, bool) + external + returns (uint256); +} + +contract StrategySEthConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // eCRV + address public constant want = + address(0xA3D87FffcE63B53E0d54fAa1cc983B7eB0b74A9c); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public voter = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public curve = + address(0xc5424B857f758E906013F3555Dad202e4bdB4567); + + uint256 public keepCRV = 0; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, '!governance'); + _; + } + + modifier onlyController() { + require(msg.sender == controller, '!controller'); + _; + } + + function() external payable { } + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0x192469CadE297D6B21F418cFA8c366b63FFC9f9b); + } + + function getName() external pure returns (string memory) { + return 'StrategySEthConvex'; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + '!authorized' + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function setVoter(address _voter) external onlyGovernance { + voter = _voter; + } + + function setWeth(address _weth) external onlyGovernance { + _weth = _weth; + } + + function setSushiRouter(address _sushiRouter) external onlyGovernance { + sushiRouter = _sushiRouter; + } + + function setCurve(address _curve) external onlyGovernance { + curve = _curve; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(23, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), 'want'); + require(cvx != address(_asset), 'cvx'); + require(crv != address(_asset), 'crv'); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + // slippageCRV = 100 for 1% max slippage + function harvest( + uint256 maxSlippageCRV, + uint256 maxSlippageCVX, + uint256 maxSlippageCRVAddLiquidity + ) public { + require( + msg.sender == strategist || msg.sender == governance, + '!authorized' + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](2); + path[0] = crv; + path[1] = weth; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = + _amounts[1].mul(10000 - maxSlippageCRV).div(10000); + + Sushi(sushiRouter).swapExactTokensForETH( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](2); + path[0] = cvx; + path[1] = weth; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = + _amounts[1].mul(10000 - maxSlippageCVX).div(10000); + + Sushi(sushiRouter).swapExactTokensForETH( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + uint256 _eth = address(this).balance; + + if (_eth > 0) { + uint256 _tokenAmount = + ICurveFi(curve).calc_token_amount([_eth, 0], true); + uint256 _minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + + ICurveFi(curve).add_liquidity.value(_eth)([_eth, 0], _minimalAmount); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/convex/contracts/StrategySbtcConvex.sol b/convex/contracts/StrategySbtcConvex.sol new file mode 100644 index 0000000..7606e98 --- /dev/null +++ b/convex/contracts/StrategySbtcConvex.sol @@ -0,0 +1,351 @@ +pragma solidity ^0.5.17; + +// yarn add @openzeppelin/contracts@2.5.1 +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[3] calldata, uint256) external; + + function calc_token_amount(uint256[3] calldata, bool) + external + returns (uint256); +} + +contract StrategySbtcConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // 3crv + address public constant want = + address(0x075b1bb99792c9E1041bA13afEf80C91a1e70fB3); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant usdc = + address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public constant wbtc = + address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + + address public constant voter = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public constant sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public constant curve = + address(0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714); + + uint256 public keepCRV = 0; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, '!governance'); + _; + } + + modifier onlyController() { + require(msg.sender == controller, '!controller'); + _; + } + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0xd727A5A6D1C7b31Ff9Db4Db4d24045B7dF0CFF93); + } + + function getName() external pure returns (string memory) { + return 'StrategyBtcConvex'; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + '!authorized' + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(7, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), 'want'); + require(cvx != address(_asset), 'cvx'); + require(crv != address(_asset), 'crv'); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + // slippageCRV = 100 for 1% max slippage + function harvest( + uint256 maxSlippageCRV, + uint256 maxSlippageCVX, + uint256 maxSlippageCRVAddLiquidity + ) public { + require( + msg.sender == strategist || msg.sender == governance, + '!authorized' + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = wbtc; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCRV).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](3); + path[0] = cvx; + path[1] = weth; + path[2] = wbtc; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCVX).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + uint256 _wbtc = IERC20(wbtc).balanceOf(address(this)); + + if (_wbtc > 0) { + IERC20(wbtc).safeApprove(curve, 0); + IERC20(wbtc).safeApprove(curve, _wbtc); + + uint256 _tokenAmount = + ICurveFi(curve).calc_token_amount([0, _wbtc, 0], true); + + uint256 _minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + ICurveFi(curve).add_liquidity([0, _wbtc, 0], _minimalAmount); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/convex/contracts/StrategyStEthConvex.sol b/convex/contracts/StrategyStEthConvex.sol new file mode 100644 index 0000000..54e539e --- /dev/null +++ b/convex/contracts/StrategyStEthConvex.sol @@ -0,0 +1,386 @@ +pragma solidity ^0.5.17; + +// yarn add @openzeppelin/contracts@2.5.1 +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForETH( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[2] calldata, uint256) external payable; + + function calc_token_amount(uint256[2] calldata, bool) + external + returns (uint256); +} + +contract StrategyStEthConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // steCRV + address public constant want = + address(0x06325440D014e39736583c165C2963BA99fAf14E); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public constant ldo = + address(0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32); + + address public voter = address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public curve = address(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022); + + uint256 public keepCRV = 0; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, '!governance'); + _; + } + + modifier onlyController() { + require(msg.sender == controller, '!controller'); + _; + } + + function() external payable {} + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0x0A760466E1B4621579a82a39CB56Dda2F4E70f03); + } + + function getName() external pure returns (string memory) { + return 'StrategyStEthConvex'; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + '!authorized' + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function setVoter(address _voter) external onlyGovernance { + voter = _voter; + } + + function setWeth(address _weth) external onlyGovernance { + _weth = _weth; + } + + function setSushiRouter(address _sushiRouter) external onlyGovernance { + sushiRouter = _sushiRouter; + } + + function setCurve(address _curve) external onlyGovernance { + curve = _curve; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(25, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), 'want'); + require(cvx != address(_asset), 'cvx'); + require(crv != address(_asset), 'crv'); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), '!vault'); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + // slippageCRV = 100 for 1% max slippage + function harvest( + uint256 maxSlippageCRV, + uint256 maxSlippageCVX, + uint256 maxSlippageLDO, + uint256 maxSlippageCRVAddLiquidity + ) public { + require( + msg.sender == strategist || msg.sender == governance, + '!authorized' + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + uint256 _ldo = IERC20(ldo).balanceOf(address(this)); + + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](2); + path[0] = crv; + path[1] = weth; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = _amounts[1].mul(10000 - maxSlippageCRV).div( + 10000 + ); + + Sushi(sushiRouter).swapExactTokensForETH( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](2); + path[0] = cvx; + path[1] = weth; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = _amounts[1].mul(10000 - maxSlippageCVX).div( + 10000 + ); + + Sushi(sushiRouter).swapExactTokensForETH( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + if (_ldo > 0) { + IERC20(ldo).safeApprove(sushiRouter, 0); + IERC20(ldo).safeApprove(sushiRouter, _ldo); + + address[] memory path = new address[](2); + path[0] = ldo; + path[1] = weth; + + uint256[] memory _amounts = Sushi(sushiRouter).getAmountsOut(_ldo, path); + uint256 _minimalAmount = _amounts[1].mul(10000 - maxSlippageLDO).div( + 10000 + ); + + Sushi(sushiRouter).swapExactTokensForETH( + _ldo, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + uint256 _eth = address(this).balance; + + if (_eth > 0) { + uint256 _tokenAmount = ICurveFi(curve).calc_token_amount([_eth, 0], true); + uint256 _minimalAmount = _tokenAmount + .mul(10000 - maxSlippageCRVAddLiquidity) + .div(10000); + + ICurveFi(curve).add_liquidity.value(_eth)([_eth, 0], _minimalAmount); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/convex/contracts/Vault.sol b/convex/contracts/Vault.sol new file mode 100644 index 0000000..2aabb2e --- /dev/null +++ b/convex/contracts/Vault.sol @@ -0,0 +1,144 @@ +pragma solidity ^0.5.17; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol'; +import '@openzeppelin/contracts/ownership/Ownable.sol'; + + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +contract Vault is ERC20, ERC20Detailed { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + IERC20 public token; + + uint256 public min = 9500; + uint256 public constant max = 10000; + + address public governance; + address public controller; + + constructor( + address _token, + address _controller, + address _governance + ) + public + ERC20Detailed( + string(abi.encodePacked('Stake DAO ', ERC20Detailed(_token).name())), + string(abi.encodePacked('sd', ERC20Detailed(_token).symbol())), + ERC20Detailed(_token).decimals() + ) + { + token = IERC20(_token); + governance = msg.sender; + controller = _controller; + governance = _governance; + } + + function balance() public view returns (uint256) { + return + token.balanceOf(address(this)).add( + IController(controller).balanceOf(address(token)) + ); + } + + function setMin(uint256 _min) external { + require(msg.sender == governance, '!governance'); + min = _min; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, '!governance'); + governance = _governance; + } + + function setController(address _controller) public { + require(msg.sender == governance, '!governance'); + controller = _controller; + } + + // Custom logic in here for how much the vault allows to be borrowed + // Sets minimum required on-hand to keep small withdrawals cheap + function available() public view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() public { + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint256 _amount) public { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + // Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + function harvest(address reserve, uint256 amount) external { + require(msg.sender == controller, '!controller'); + require(reserve != address(token), 'token'); + IERC20(reserve).safeTransfer(controller, amount); + } + + // No rebalance implementation for lower fees and faster swaps + function withdraw(uint256 _shares) public { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _withdraw = r.sub(b); + IController(controller).withdraw(address(token), _withdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + token.safeTransfer(msg.sender, r); + } + + function getPricePerFullShare() public view returns (uint256) { + return totalSupply() == 0 ? 1e18 : balance().mul(1e18).div(totalSupply()); + } +} diff --git a/convex/hardhat.config.ts b/convex/hardhat.config.ts new file mode 100644 index 0000000..7b2e452 --- /dev/null +++ b/convex/hardhat.config.ts @@ -0,0 +1,42 @@ +import {HardhatUserConfig} from 'hardhat/config'; +import '@nomiclabs/hardhat-ethers'; +import '@nomiclabs/hardhat-waffle'; +import '@nomiclabs/hardhat-etherscan'; +import 'hardhat-tracer'; + +import './tasks/deploy-usd'; +import './tasks/deploy-btc'; +import './tasks/deploy-eur'; +import './tasks/deploy-frx'; +import './tasks/deploy-steth'; + +require('dotenv').config(); + +const DEPLOYER = process.env.DEPLOYER; + +export default { + defaultNetwork: 'hardhat', + networks: { + hardhat: { + forking: { + url: process.env.ALCHEMY_MAINNET, + }, + }, + mainnet: { + url: process.env.ALCHEMY_MAINNET, + accounts: [`0x${DEPLOYER}`], + gasPrice: 60000000000, + }, + }, + solidity: { + compilers: [ + {version: '0.8.0'}, + {version: '0.7.4'}, + {version: '0.6.12'}, + {version: '0.5.17'}, + ], + }, + etherscan: { + apiKey: process.env.ETHERSCAN_KEY, + }, +} as HardhatUserConfig; diff --git a/convex/package.json b/convex/package.json new file mode 100644 index 0000000..dcc722a --- /dev/null +++ b/convex/package.json @@ -0,0 +1,35 @@ +{ + "name": "convex-test", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "@nomiclabs/hardhat-etherscan": "^2.1.2", + "@nomiclabs/hardhat-waffle": "^2.0.0", + "@types/chai": "^4.2.17", + "@types/mocha": "^8.2.2", + "@types/node": "^15.0.1", + "chai": "^4.3.4", + "ethereum-waffle": "^3.0.0", + "ethers": "^5.0.0", + "hardhat": "^2.2.1", + "hardhat-abi-exporter": "^2.2.1", + "prettier-plugin-solidity": "^1.0.0-beta.10", + "ts-node": "^9.1.1", + "typescript": "^4.2.4" + }, + "dependencies": { + "@0xsequence/erc-1155": "^3.0.4", + "@chainlink/contracts": "^0.1.7", + "@ethersproject/units": "^5.3.0", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@openzeppelin/contracts": "2.5.1", + "@uniswap/v3-periphery": "^1.1.0", + "dotenv": "^8.2.0", + "hardhat-tracer": "1.0.0-alpha.6", + "prettier": "^2.3.0" + }, + "scripts": { + "prettier": "prettier --write 'contracts/**/*.sol'" + } +} diff --git a/convex/readme.md b/convex/readme.md new file mode 100644 index 0000000..9e92ae2 --- /dev/null +++ b/convex/readme.md @@ -0,0 +1,3 @@ +## Run test + +`npx hardhat test test/strategy.ts` diff --git a/convex/test/strategy3Crv.ts b/convex/test/strategy3Crv.ts new file mode 100644 index 0000000..2e92fca --- /dev/null +++ b/convex/test/strategy3Crv.ts @@ -0,0 +1,236 @@ +import {ethers, network} from 'hardhat'; +import {expect} from 'chai'; +import {BigNumber} from '@ethersproject/bignumber'; +import {Contract} from '@ethersproject/contracts'; +import {parseEther} from '@ethersproject/units'; +import {JsonRpcSigner} from '@ethersproject/providers'; + +import Controller from '../abis/Controller.json'; +import YVault from '../abis/YVault.json'; +import ERC20 from '../abis/ERC20.json'; + +const CONTROLLER = '0x29D3782825432255041Db2EAfCB7174f5273f08A'; +const VAULT = '0xB17640796e4c27a39AF51887aff3F8DC0daF9567'; +const WANT = '0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490'; +const GOVERNANCE = '0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063'; +const WANT_HOLDER = '0x89515406c15a277f8906090553366219b3639834'; + +const PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A'; +const RANDOM = '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8'; + +const VE_CRV = '0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2'; +const CURVE_VOTER = '0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6'; + +const VEABI = [ + { + name: 'locked', + outputs: [ + {type: 'int128', name: 'amount'}, + {type: 'uint256', name: 'end'} + ], + inputs: [{type: 'address', name: 'arg0'}], + stateMutability: 'view', + type: 'function', + gas: 3359 + } +]; + +describe('Convex3CRV', function () { + let controller: Contract; + let strategy: Contract; + let vault: Contract; + let want: Contract; + let veCRV: Contract; + let wantHolder: JsonRpcSigner; + let governance: JsonRpcSigner; + let controllerSigner: JsonRpcSigner; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [GOVERNANCE] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [WANT_HOLDER] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [CONTROLLER] + }); + + const Strategy = await ethers.getContractFactory('Strategy3CrvConvex'); + + governance = await ethers.provider.getSigner(GOVERNANCE); + wantHolder = await ethers.provider.getSigner(WANT_HOLDER); + controllerSigner = await ethers.provider.getSigner(CONTROLLER); + strategy = await Strategy.deploy(CONTROLLER, PROXY); + controller = await ethers.getContractAt(Controller, CONTROLLER); + vault = await ethers.getContractAt(YVault, VAULT); + want = await ethers.getContractAt(ERC20, WANT); + veCRV = await ethers.getContractAt(VEABI, VE_CRV); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, strategy.address); + + console.log('approveStrategy :', tx1.hash); + await tx1.wait(); + + const tx2 = await controller + .connect(governance) + .setStrategy(WANT, strategy.address); + + console.log('setStrategy :', tx2.hash); + await tx2.wait(); + }); + + it('All funds should be in vault', async function () { + const vaultBalance = await vault.balance(); + const vault3crBalance = await want.balanceOf(vault.address); + expect(vaultBalance.eq(vault3crBalance)).to.be.true; + }); + + it('should deposit 95% of vault balance in convex 3pool', async function () { + this.enableTimeouts(false); + const vaultBalance = await vault.balance(); + const tx = await vault.earn(); + await tx.wait(); + + const balance = await strategy.balanceOf(); + const amountExpected = vaultBalance.mul(95).div(100); + + expect(balance.eq(amountExpected)).to.be.true; + }); + + it('should lock CRV and ensure we have more LP in baseRewardPool', async function () { + this.enableTimeouts(false); + + const {amount: veCRVBalanceBefore} = await veCRV.locked(CURVE_VOTER); + + ethers.provider.send('evm_increaseTime', [3600]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOfPool(); + const tx = await strategy.harvest(100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOfPool(); + + const {amount: veCRVBalanceAfter} = await veCRV.locked(CURVE_VOTER); + const hasLockedCRV = veCRVBalanceAfter.gt(veCRVBalanceBefore); + + expect(hasLockedCRV).to.be.true; // ensure that we lock CRV + expect(balanceAfter.gt(balanceBefore)).to.be.true; // + }); + + it('should harvest', async function () { + this.enableTimeouts(false); + + ethers.provider.send('evm_increaseTime', [3600 * 24]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOf(); + const tx = await strategy.harvest(100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOf(); + + expect(balanceAfter.gt(balanceBefore)).to.be.true; + }); + + it('should withdraw some for the user', async function () { + this.enableTimeouts(false); + const balanceBefore = await strategy.balanceOf(); + const amountDeposited = await want.balanceOf(WANT_HOLDER); + + const tx0 = await want + .connect(wantHolder) + .approve(vault.address, amountDeposited); + + await tx0.wait(); + + const tx1 = await vault.connect(wantHolder).deposit(amountDeposited); + await tx1.wait(); + + // triple earn to leave close to nothing in vault (5% always stay with earn) + const tx2 = await vault.earn(); + await tx2.wait(); + const tx2b = await vault.earn(); + await tx2b.wait(); + const tx2c = await vault.earn(); + await tx2c.wait(); + + //available in the vault (95%) that can be sent to strat + //const vaultAvailableAfterEarn = await vault.available(); + const vaultAvailableAfterEarn = await want.balanceOf(VAULT); + + const balanceAfterEarn = await strategy.balanceOf(); + + const sdBalance = await vault.balanceOf(WANT_HOLDER); + const pricePerShare = await vault.getPricePerFullShare(); + + const getInWant = (am: BigNumber): any => { + return am + .mul(pricePerShare) + .div(BigNumber.from(1).mul(BigNumber.from(10).pow(18))); + }; + + const withdrawAmount = sdBalance.div(4); + const withdrawAmountWant = getInWant(withdrawAmount); + + const userWantBalanceBefore = await want.balanceOf(WANT_HOLDER); + + const tx3 = await vault.connect(wantHolder).withdraw(withdrawAmount); + await tx3.wait(); + + const balanceAfterWithdraw = await strategy.balanceOf(); + const userWantBalanceAfter = await want.balanceOf(WANT_HOLDER); + + const expectedWithdrawFromStrategy = withdrawAmountWant.sub( + vaultAvailableAfterEarn + ); + + const balanceDifference = balanceAfterEarn.sub(balanceAfterWithdraw); + + // 0.5% user withdrawal paid fees + const withdrawalFee = withdrawAmountWant + .sub(vaultAvailableAfterEarn) + .mul(5) + .div(1000); + + const expectedUserBalanceWantAfter = withdrawAmountWant + .sub(withdrawalFee) + .add(userWantBalanceBefore); + }); + + it('should withdraw all', async function () { + this.enableTimeouts(false); + + const balanceBefore = await strategy.balanceOf(); + const vaultBalanceBefore = await want.balanceOf(VAULT); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, RANDOM); + + await tx1.wait(); + + // replace strat by random strat + const tx2 = await controller.connect(governance).setStrategy(WANT, RANDOM); + + await tx2.wait(); + + const balanceAfter = await strategy.balanceOf(); + const vaultBalanceAfter = await want.balanceOf(VAULT); + + expect(balanceBefore.gt(0)).to.be.true; + expect(balanceAfter.eq(0)).to.be.true; + expect(vaultBalanceAfter.gt(vaultBalanceBefore)).to.be.true; + expect(vaultBalanceBefore.add(balanceBefore).eq(vaultBalanceAfter)).to.be + .true; + }); +}); diff --git a/convex/test/strategyEurs.ts b/convex/test/strategyEurs.ts new file mode 100644 index 0000000..8ae80a7 --- /dev/null +++ b/convex/test/strategyEurs.ts @@ -0,0 +1,239 @@ +import {ethers, network} from 'hardhat'; +import {expect} from 'chai'; +import {BigNumber} from '@ethersproject/bignumber'; +import {Contract} from '@ethersproject/contracts'; +import {parseEther} from '@ethersproject/units'; +import {JsonRpcSigner} from '@ethersproject/providers'; + +import Controller from '../abis/Controller.json'; +import YVault from '../abis/YVault.json'; +import ERC20 from '../abis/ERC20.json'; + +const CONTROLLER = '0x29D3782825432255041Db2EAfCB7174f5273f08A'; +const VAULT = '0xCD6997334867728ba14d7922f72c893fcee70e84'; // eur vault +const WANT = '0x194eBd173F6cDacE046C53eACcE9B953F28411d1'; // eursCRV +const GOVERNANCE = '0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063'; +const WANT_HOLDER = '0xF872Ea3e3BC2d9EFcb660dE497A6F1c50E4ad25D'; // bug eursCRV holder + +const PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A'; +const RANDOM = '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8'; + +const VE_CRV = '0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2'; +const CURVE_VOTER = '0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6'; + +const VEABI = [ + { + name: 'locked', + outputs: [ + {type: 'int128', name: 'amount'}, + {type: 'uint256', name: 'end'} + ], + inputs: [{type: 'address', name: 'arg0'}], + stateMutability: 'view', + type: 'function', + gas: 3359 + } +]; + +describe('ConvexEur', function () { + let controller: Contract; + let strategy: Contract; + let vault: Contract; + let want: Contract; + let veCRV: Contract; + let wantHolder: JsonRpcSigner; + let governance: JsonRpcSigner; + let controllerSigner: JsonRpcSigner; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [GOVERNANCE] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [WANT_HOLDER] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [CONTROLLER] + }); + + const Strategy = await ethers.getContractFactory('StrategyEursConvex'); + + governance = await ethers.provider.getSigner(GOVERNANCE); + wantHolder = await ethers.provider.getSigner(WANT_HOLDER); + controllerSigner = await ethers.provider.getSigner(CONTROLLER); + strategy = await Strategy.deploy(CONTROLLER, PROXY); + controller = await ethers.getContractAt(Controller, CONTROLLER); + vault = await ethers.getContractAt(YVault, VAULT); + want = await ethers.getContractAt(ERC20, WANT); + veCRV = await ethers.getContractAt(VEABI, VE_CRV); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, strategy.address); + + console.log('approveStrategy :', tx1.hash); + await tx1.wait(); + + const tx2 = await controller + .connect(governance) + .setStrategy(WANT, strategy.address); + + console.log('setStrategy :', tx2.hash); + await tx2.wait(); + }); + + it('All funds should be in vault', async function () { + const vaultBalance = await vault.balance(); + const vaultWantBalance = await want.balanceOf(vault.address); + expect(vaultBalance.eq(vaultWantBalance)).to.be.true; + }); + + it('should deposit 95% of vault balance in convex eur', async function () { + this.enableTimeouts(false); + const vaultBalance = await vault.balance(); + const tx = await vault.earn(); + await tx.wait(); + + const balance = await strategy.balanceOf(); + const amountExpected = vaultBalance.mul(95).div(100); + + expect(balance.eq(amountExpected)).to.be.true; + }); + + it('should lock CRV and ensure we have more LP in baseRewardPool', async function () { + this.enableTimeouts(false); + + const {amount: veCRVBalanceBefore} = await veCRV.locked(CURVE_VOTER); + + ethers.provider.send('evm_increaseTime', [3600]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOfPool(); + const tx = await strategy.harvest(100, 100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOfPool(); + + const {amount: veCRVBalanceAfter} = await veCRV.locked(CURVE_VOTER); + const hasLockedCRV = veCRVBalanceAfter.gt(veCRVBalanceBefore); + + expect(hasLockedCRV).to.be.true; // ensure that we lock CRV + expect(balanceAfter.gt(balanceBefore)).to.be.true; // + }); + + it('should harvest', async function () { + this.enableTimeouts(false); + + ethers.provider.send('evm_increaseTime', [3600 * 24]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOf(); + const tx = await strategy.harvest(100, 100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOf(); + + expect(balanceAfter.gt(balanceBefore)).to.be.true; + }); + + it('should withdraw some for the user', async function () { + this.enableTimeouts(false); + const balanceBefore = await strategy.balanceOf(); + const amountDeposited = await want.balanceOf(WANT_HOLDER); + + const tx0 = await want + .connect(wantHolder) + .approve(vault.address, amountDeposited); + + await tx0.wait(); + + const tx1 = await vault.connect(wantHolder).deposit(amountDeposited); + await tx1.wait(); + + // triple earn to leave close to nothing in vault (5% always stay with earn) + const tx2 = await vault.earn(); + await tx2.wait(); + const tx2b = await vault.earn(); + await tx2b.wait(); + const tx2c = await vault.earn(); + await tx2c.wait(); + + //available in the vault (95%) that can be sent to strat + //const vaultAvailableAfterEarn = await vault.available(); + const vaultAvailableAfterEarn = await want.balanceOf(VAULT); + + const balanceAfterEarn = await strategy.balanceOf(); + + const sdBalance = await vault.balanceOf(WANT_HOLDER); + const pricePerShare = await vault.getPricePerFullShare(); + + const getInWant = (am: BigNumber): any => { + return am + .mul(pricePerShare) + .div(BigNumber.from(1).mul(BigNumber.from(10).pow(18))); + }; + + const withdrawAmount = sdBalance.div(4); + const withdrawAmountWant = getInWant(withdrawAmount); + + const userWantBalanceBefore = await want.balanceOf(WANT_HOLDER); + + const tx3 = await vault.connect(wantHolder).withdraw(withdrawAmount); + await tx3.wait(); + + const balanceAfterWithdraw = await strategy.balanceOf(); + const userWantBalanceAfter = await want.balanceOf(WANT_HOLDER); + + const expectedWithdrawFromStrategy = withdrawAmountWant.sub( + vaultAvailableAfterEarn + ); + + const balanceDifference = balanceAfterEarn.sub(balanceAfterWithdraw); + + // 0.5% user withdrawal paid fees + const withdrawalFee = withdrawAmountWant + .sub(vaultAvailableAfterEarn) + .mul(5) + .div(1000); + + const expectedUserBalanceWantAfter = withdrawAmountWant + .sub(withdrawalFee) + .add(userWantBalanceBefore); + + expect(balanceDifference.gt(0)); + expect(userWantBalanceAfter.gt(userWantBalanceBefore)); + }); + + it('should withdraw all', async function () { + this.enableTimeouts(false); + + const balanceBefore = await strategy.balanceOf(); + const vaultBalanceBefore = await want.balanceOf(VAULT); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, RANDOM); + + await tx1.wait(); + + // replace strat by random strat + const tx2 = await controller.connect(governance).setStrategy(WANT, RANDOM); + + await tx2.wait(); + + const balanceAfter = await strategy.balanceOf(); + const vaultBalanceAfter = await want.balanceOf(VAULT); + + expect(balanceBefore.gt(0)).to.be.true; + expect(balanceAfter.eq(0)).to.be.true; + expect(vaultBalanceAfter.gt(vaultBalanceBefore)).to.be.true; + expect(vaultBalanceBefore.add(balanceBefore).eq(vaultBalanceAfter)).to.be + .true; + }); +}); diff --git a/convex/test/strategyEurt.ts b/convex/test/strategyEurt.ts new file mode 100644 index 0000000..dc0203b --- /dev/null +++ b/convex/test/strategyEurt.ts @@ -0,0 +1,259 @@ +import {ethers, network} from 'hardhat'; +import {expect} from 'chai'; +import {BigNumber} from '@ethersproject/bignumber'; +import {Contract} from '@ethersproject/contracts'; +import {parseEther} from '@ethersproject/units'; +import {JsonRpcSigner} from '@ethersproject/providers'; + +import Controller from '../abis/Controller.json'; +import YVault from '../abis/YVault.json'; +import ERC20 from '../abis/ERC20.json'; + +const CONTROLLER = '0x29D3782825432255041Db2EAfCB7174f5273f08A'; +const VAULT = '0xCD6997334867728ba14d7922f72c893fcee70e84'; // eur vault +const WANT = '0xFD5dB7463a3aB53fD211b4af195c5BCCC1A03890'; // eurtCRV +const GOVERNANCE = '0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063'; +const WANT_HOLDER = '0x93c937a619657e25b2df27586fc91356278759f3'; // bug eurtCRV holder + +const PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A'; +const RANDOM = '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8'; + +const VE_CRV = '0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2'; +const CURVE_VOTER = '0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6'; + +const VEABI = [ + { + name: 'locked', + outputs: [ + {type: 'int128', name: 'amount'}, + {type: 'uint256', name: 'end'} + ], + inputs: [{type: 'address', name: 'arg0'}], + stateMutability: 'view', + type: 'function', + gas: 3359 + } +]; + +describe('ConvexEur', function () { + let controller: Contract; + let strategy: Contract; + let vault: Contract; + let want: Contract; + let veCRV: Contract; + let wantHolder: JsonRpcSigner; + let governance: JsonRpcSigner; + let controllerSigner: JsonRpcSigner; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [GOVERNANCE] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [WANT_HOLDER] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [CONTROLLER] + }); + + const Vault = await ethers.getContractFactory('Vault'); + const Strategy = await ethers.getContractFactory('StrategyEurtConvex'); + + governance = await ethers.provider.getSigner(GOVERNANCE); + wantHolder = await ethers.provider.getSigner(WANT_HOLDER); + controllerSigner = await ethers.provider.getSigner(CONTROLLER); + + vault = await Vault.deploy(WANT, CONTROLLER, GOVERNANCE); + strategy = await Strategy.deploy(CONTROLLER, PROXY); + controller = await ethers.getContractAt(Controller, CONTROLLER); + + want = await ethers.getContractAt(ERC20, WANT); + veCRV = await ethers.getContractAt(VEABI, VE_CRV); + + const tx0 = await controller + .connect(governance) + .setVault(WANT, vault.address); + + console.log('setVault :', tx0.hash); + await tx0.wait(); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, strategy.address); + + console.log('approveStrategy :', tx1.hash); + await tx1.wait(); + + const tx2 = await controller + .connect(governance) + .setStrategy(WANT, strategy.address); + + console.log('setStrategy :', tx2.hash); + await tx2.wait(); + + /* + SIMULATE INITIAL DEPOSIT IN THE VAULT + */ + const holderBalance = await want.balanceOf(WANT_HOLDER); + await ( + await want.connect(wantHolder).approve(vault.address, holderBalance) + ).wait(); + + await (await vault.connect(wantHolder).deposit(holderBalance)).wait(); + }); + + it('All funds should be in vault', async function () { + const vaultBalance = await vault.balance(); + const vaultWantBalance = await want.balanceOf(vault.address); + expect(vaultBalance.eq(vaultWantBalance)).to.be.true; + }); + + it('should deposit 95% of vault balance in convex eur', async function () { + this.enableTimeouts(false); + const vaultBalance = await vault.balance(); + const tx = await vault.earn(); + await tx.wait(); + + const balance = await strategy.balanceOf(); + const amountExpected = vaultBalance.mul(95).div(100); + + expect(balance.eq(amountExpected)).to.be.true; + }); + + it('should lock CRV and ensure we have more LP in baseRewardPool', async function () { + this.enableTimeouts(false); + + const {amount: veCRVBalanceBefore} = await veCRV.locked(CURVE_VOTER); + + ethers.provider.send('evm_increaseTime', [3600]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOfPool(); + const tx = await strategy.harvest(100, 100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOfPool(); + + const {amount: veCRVBalanceAfter} = await veCRV.locked(CURVE_VOTER); + const hasLockedCRV = veCRVBalanceAfter.gt(veCRVBalanceBefore); + + expect(hasLockedCRV).to.be.true; // ensure that we lock CRV + expect(balanceAfter.gt(balanceBefore)).to.be.true; // + }); + + it('should harvest', async function () { + this.enableTimeouts(false); + + ethers.provider.send('evm_increaseTime', [3600 * 24]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOf(); + const tx = await strategy.harvest(100, 100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOf(); + + expect(balanceAfter.gt(balanceBefore)).to.be.true; + }); + + it('should withdraw some for the user', async function () { + this.enableTimeouts(false); + const balanceBefore = await strategy.balanceOf(); + const amountDeposited = await want.balanceOf(WANT_HOLDER); + + const tx0 = await want + .connect(wantHolder) + .approve(vault.address, amountDeposited); + + await tx0.wait(); + + const tx1 = await vault.connect(wantHolder).deposit(amountDeposited); + await tx1.wait(); + + // triple earn to leave close to nothing in vault (5% always stay with earn) + const tx2 = await vault.earn(); + await tx2.wait(); + const tx2b = await vault.earn(); + await tx2b.wait(); + const tx2c = await vault.earn(); + await tx2c.wait(); + + //available in the vault (95%) that can be sent to strat + //const vaultAvailableAfterEarn = await vault.available(); + const vaultAvailableAfterEarn = await want.balanceOf(VAULT); + + const balanceAfterEarn = await strategy.balanceOf(); + + const sdBalance = await vault.balanceOf(WANT_HOLDER); + const pricePerShare = await vault.getPricePerFullShare(); + + const getInWant = (am: BigNumber): any => { + return am + .mul(pricePerShare) + .div(BigNumber.from(1).mul(BigNumber.from(10).pow(18))); + }; + + const withdrawAmount = sdBalance.div(4); + const withdrawAmountWant = getInWant(withdrawAmount); + + const userWantBalanceBefore = await want.balanceOf(WANT_HOLDER); + + const tx3 = await vault.connect(wantHolder).withdraw(withdrawAmount); + await tx3.wait(); + + const balanceAfterWithdraw = await strategy.balanceOf(); + const userWantBalanceAfter = await want.balanceOf(WANT_HOLDER); + + const expectedWithdrawFromStrategy = withdrawAmountWant.sub( + vaultAvailableAfterEarn + ); + + const balanceDifference = balanceAfterEarn.sub(balanceAfterWithdraw); + + // 0.5% user withdrawal paid fees + const withdrawalFee = withdrawAmountWant + .sub(vaultAvailableAfterEarn) + .mul(5) + .div(1000); + + const expectedUserBalanceWantAfter = withdrawAmountWant + .sub(withdrawalFee) + .add(userWantBalanceBefore); + + expect(balanceDifference.gt(0)); + expect(userWantBalanceAfter.gt(userWantBalanceBefore)); + }); + + it('should withdraw all', async function () { + this.enableTimeouts(false); + + const balanceBefore = await strategy.balanceOf(); + const vaultBalanceBefore = await want.balanceOf(VAULT); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, RANDOM); + + await tx1.wait(); + + // replace strat by random strat + const tx2 = await controller.connect(governance).setStrategy(WANT, RANDOM); + + await tx2.wait(); + + const balanceAfter = await strategy.balanceOf(); + const vaultBalanceAfter = await want.balanceOf(VAULT); + + expect(balanceBefore.gt(0)).to.be.true; + expect(balanceAfter.eq(0)).to.be.true; + expect(vaultBalanceAfter.gt(vaultBalanceBefore)).to.be.true; + expect(vaultBalanceBefore.add(balanceBefore).eq(vaultBalanceAfter)).to.be + .true; + }); +}); diff --git a/convex/test/strategyFrx.ts b/convex/test/strategyFrx.ts new file mode 100644 index 0000000..b0333e2 --- /dev/null +++ b/convex/test/strategyFrx.ts @@ -0,0 +1,256 @@ +import {ethers, network} from 'hardhat'; +import {expect} from 'chai'; +import {BigNumber} from '@ethersproject/bignumber'; +import {Contract} from '@ethersproject/contracts'; +import {parseEther} from '@ethersproject/units'; +import {JsonRpcSigner} from '@ethersproject/providers'; + +import Controller from '../abis/Controller.json'; +import YVault from '../abis/YVault.json'; +import ERC20 from '../abis/ERC20.json'; + +const CONTROLLER = '0x29D3782825432255041Db2EAfCB7174f5273f08A'; +const VAULT = '0x99780beAdd209cc3c7282536883Ef58f4ff4E52F'; +const WANT = '0xd632f22692FaC7611d2AA1C0D552930D43CAEd3B'; // frax LP +const GOVERNANCE = '0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063'; +const WANT_HOLDER = '0x07A75Ba044cDAaa624aAbAD27CB95C42510AF4B5'; // bug eursCRV holder + +const PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A'; +const RANDOM = '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8'; + +const VE_CRV = '0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2'; +const CURVE_VOTER = '0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6'; + +const VEABI = [ + { + name: 'locked', + outputs: [ + {type: 'int128', name: 'amount'}, + {type: 'uint256', name: 'end'} + ], + inputs: [{type: 'address', name: 'arg0'}], + stateMutability: 'view', + type: 'function', + gas: 3359 + } +]; + +describe('ConvexFrx', function () { + let controller: Contract; + let strategy: Contract; + let vault: Contract; + let want: Contract; + let veCRV: Contract; + let wantHolder: JsonRpcSigner; + let governance: JsonRpcSigner; + let controllerSigner: JsonRpcSigner; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [GOVERNANCE] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [WANT_HOLDER] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [CONTROLLER] + }); + + const Strategy = await ethers.getContractFactory('StrategyFrxConvex'); + + governance = await ethers.provider.getSigner(GOVERNANCE); + wantHolder = await ethers.provider.getSigner(WANT_HOLDER); + controllerSigner = await ethers.provider.getSigner(CONTROLLER); + strategy = await Strategy.deploy(CONTROLLER, PROXY); + controller = await ethers.getContractAt(Controller, CONTROLLER); + vault = await ethers.getContractAt(YVault, VAULT); + want = await ethers.getContractAt(ERC20, WANT); + veCRV = await ethers.getContractAt(VEABI, VE_CRV); + + const tx0 = await controller + .connect(governance) + .setVault(WANT, vault.address); + + console.log('setVault :', tx0.hash); + await tx0.wait(); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, strategy.address); + + console.log('approveStrategy :', tx1.hash); + await tx1.wait(); + + const tx2 = await controller + .connect(governance) + .setStrategy(WANT, strategy.address); + + console.log('setStrategy :', tx2.hash); + await tx2.wait(); + + /* + SIMULATE INITIAL DEPOSIT IN THE VAULT + */ + const holderBalance = await want.balanceOf(WANT_HOLDER); + await ( + await want.connect(wantHolder).approve(vault.address, holderBalance) + ).wait(); + + await (await vault.connect(wantHolder).deposit(holderBalance)).wait(); + }); + + it('All funds should be in vault', async function () { + const vaultBalance = await vault.balance(); + const vaultWantBalance = await want.balanceOf(vault.address); + expect(vaultBalance.eq(vaultWantBalance)).to.be.true; + }); + + it('should deposit 95% of vault balance in convex frax', async function () { + this.enableTimeouts(false); + const vaultBalance = await vault.balance(); + const tx = await vault.earn(); + await tx.wait(); + + const balance = await strategy.balanceOf(); + const amountExpected = vaultBalance.mul(95).div(100); + + expect(balance.eq(amountExpected)).to.be.true; + }); + + it('should lock all CRV and ensure we have more LP in baseRewardPool', async function () { + this.enableTimeouts(false); + + const {amount: veCRVBalanceBefore} = await veCRV.locked(CURVE_VOTER); + + ethers.provider.send('evm_increaseTime', [3600]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOfPool(); + const tx = await strategy.harvest(100, 100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOfPool(); + + const {amount: veCRVBalanceAfter} = await veCRV.locked(CURVE_VOTER); + const hasLockedCRV = veCRVBalanceAfter.gt(veCRVBalanceBefore); + + expect(hasLockedCRV).to.be.true; // ensure that we lock CRV + expect(balanceAfter.gt(balanceBefore)).to.be.true; // + }); + + it('should harvest', async function () { + this.enableTimeouts(false); + + ethers.provider.send('evm_increaseTime', [3600 * 24]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOf(); + const tx = await strategy.harvest(100, 100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOf(); + + expect(balanceAfter.gt(balanceBefore)).to.be.true; + }); + + it('should withdraw some for the user', async function () { + this.enableTimeouts(false); + const balanceBefore = await strategy.balanceOf(); + const amountDeposited = await want.balanceOf(WANT_HOLDER); + + const tx0 = await want + .connect(wantHolder) + .approve(vault.address, amountDeposited); + + await tx0.wait(); + + const tx1 = await vault.connect(wantHolder).deposit(amountDeposited); + await tx1.wait(); + + // triple earn to leave close to nothing in vault (5% always stay with earn) + const tx2 = await vault.earn(); + await tx2.wait(); + const tx2b = await vault.earn(); + await tx2b.wait(); + const tx2c = await vault.earn(); + await tx2c.wait(); + + //available in the vault (95%) that can be sent to strat + //const vaultAvailableAfterEarn = await vault.available(); + const vaultAvailableAfterEarn = await want.balanceOf(VAULT); + + const balanceAfterEarn = await strategy.balanceOf(); + + const sdBalance = await vault.balanceOf(WANT_HOLDER); + const pricePerShare = await vault.getPricePerFullShare(); + + const getInWant = (am: BigNumber): any => { + return am + .mul(pricePerShare) + .div(BigNumber.from(1).mul(BigNumber.from(10).pow(18))); + }; + + const withdrawAmount = sdBalance.div(4); + const withdrawAmountWant = getInWant(withdrawAmount); + + const userWantBalanceBefore = await want.balanceOf(WANT_HOLDER); + + const tx3 = await vault.connect(wantHolder).withdraw(withdrawAmount); + await tx3.wait(); + + const balanceAfterWithdraw = await strategy.balanceOf(); + const userWantBalanceAfter = await want.balanceOf(WANT_HOLDER); + + const expectedWithdrawFromStrategy = withdrawAmountWant.sub( + vaultAvailableAfterEarn + ); + + const balanceDifference = balanceAfterEarn.sub(balanceAfterWithdraw); + + // 0.5% user withdrawal paid fees + const withdrawalFee = withdrawAmountWant + .sub(vaultAvailableAfterEarn) + .mul(5) + .div(1000); + + const expectedUserBalanceWantAfter = withdrawAmountWant + .sub(withdrawalFee) + .add(userWantBalanceBefore); + + expect(balanceDifference.gt(0)); + expect(userWantBalanceAfter.gt(userWantBalanceBefore)); + }); + + it('should withdraw all', async function () { + this.enableTimeouts(false); + + const balanceBefore = await strategy.balanceOf(); + const vaultBalanceBefore = await want.balanceOf(VAULT); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, RANDOM); + + await tx1.wait(); + + // replace strat by random strat + const tx2 = await controller.connect(governance).setStrategy(WANT, RANDOM); + + await tx2.wait(); + + const balanceAfter = await strategy.balanceOf(); + const vaultBalanceAfter = await want.balanceOf(VAULT); + + expect(balanceBefore.gt(0)).to.be.true; + expect(balanceAfter.eq(0)).to.be.true; + expect(vaultBalanceAfter.gt(vaultBalanceBefore)).to.be.true; + expect(vaultBalanceBefore.add(balanceBefore).eq(vaultBalanceAfter)).to.be + .true; + }); +}); diff --git a/convex/test/strategySEth.ts b/convex/test/strategySEth.ts new file mode 100644 index 0000000..97a8c86 --- /dev/null +++ b/convex/test/strategySEth.ts @@ -0,0 +1,271 @@ +import {ethers, network} from 'hardhat'; +import {expect} from 'chai'; +import {BigNumber} from '@ethersproject/bignumber'; +import {Contract} from '@ethersproject/contracts'; +import {parseEther} from '@ethersproject/units'; +import {JsonRpcSigner} from '@ethersproject/providers'; + +import Controller from '../abis/Controller.json'; +import ERC20 from '../abis/ERC20.json'; + +const CONTROLLER = '0x29D3782825432255041Db2EAfCB7174f5273f08A'; +const WANT = '0xA3D87FffcE63B53E0d54fAa1cc983B7eB0b74A9c'; +const GOVERNANCE = '0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063'; +const WANT_HOLDER = '0x995a09ed0b24Ee13FbfCFbe60cad2fB6281B479F'; + +const PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A'; +const RANDOM = '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8'; + +const VE_CRV = '0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2'; +const CURVE_VOTER = '0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6'; + +const VEABI = [ + { + name: 'locked', + outputs: [ + {type: 'int128', name: 'amount'}, + {type: 'uint256', name: 'end'} + ], + inputs: [{type: 'address', name: 'arg0'}], + stateMutability: 'view', + type: 'function', + gas: 3359 + } +]; + +describe('ConvexSeth', function () { + let controller: Contract; + let strategy: Contract; + let vault: Contract; + let want: Contract; + let veCRV: Contract; + let wantHolder: JsonRpcSigner; + let governance: JsonRpcSigner; + let controllerSigner: JsonRpcSigner; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [GOVERNANCE] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [WANT_HOLDER] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [CONTROLLER] + }); + + const Vault = await ethers.getContractFactory('Vault'); + const Strategy = await ethers.getContractFactory('StrategySEthConvex'); + + governance = await ethers.provider.getSigner(GOVERNANCE); + wantHolder = await ethers.provider.getSigner(WANT_HOLDER); + controllerSigner = await ethers.provider.getSigner(CONTROLLER); + + strategy = await Strategy.deploy(CONTROLLER, PROXY); + vault = await Vault.deploy(WANT, CONTROLLER, GOVERNANCE); + + controller = await ethers.getContractAt(Controller, CONTROLLER); + + want = await ethers.getContractAt(ERC20, WANT); + veCRV = await ethers.getContractAt(VEABI, VE_CRV); + + const tx0 = await controller + .connect(governance) + .setVault(WANT, vault.address); + + console.log('setVault :', tx0.hash); + await tx0.wait(); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, strategy.address); + + console.log('approveStrategy :', tx1.hash); + await tx1.wait(); + + const tx2 = await controller + .connect(governance) + .setStrategy(WANT, strategy.address); + + console.log('setStrategy :', tx2.hash); + await tx2.wait(); + + /* + SIMULATE INITIAL DEPOSIT IN THE VAULT + */ + const holderBalance = await want.balanceOf(WANT_HOLDER); + await ( + await want.connect(wantHolder).approve(vault.address, holderBalance) + ).wait(); + + await (await vault.connect(wantHolder).deposit(holderBalance)).wait(); + }); + + it('All funds should be in vault', async function () { + const vaultBalance = await vault.balance(); + const vaulteCRVBalance = await want.balanceOf(vault.address); + + console.log({ + vaultBalance: vaultBalance.toString(), + vaulteCRVBalance: vaulteCRVBalance.toString() + }); + + expect(vaultBalance.eq(vaulteCRVBalance)).to.be.true; + }); + + it('should deposit 95% of vault balance in convex sEth', async function () { + this.enableTimeouts(false); + const vaultBalance = await vault.balance(); + + console.log({ + vaultBalance: vaultBalance.toString() + }); + + const tx = await vault.earn(); + await tx.wait(); + + const balance = await strategy.balanceOf(); + const amountExpected = vaultBalance.mul(95).div(100); + + console.log({ + balance: balance.toString(), + amountExpected: amountExpected.toString() + }); + + expect(balance.eq(amountExpected)).to.be.true; + }); + + it('should lock CRV and ensure we have more LP in baseRewardPool', async function () { + this.enableTimeouts(false); + + const {amount: veCRVBalanceBefore} = await veCRV.locked(CURVE_VOTER); + + ethers.provider.send('evm_increaseTime', [3600]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOfPool(); + const tx = await strategy.harvest(100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOfPool(); + + const {amount: veCRVBalanceAfter} = await veCRV.locked(CURVE_VOTER); + const hasLockedCRV = veCRVBalanceAfter.gt(veCRVBalanceBefore); + + expect(hasLockedCRV).to.be.true; // ensure that we lock CRV + expect(balanceAfter.gt(balanceBefore)).to.be.true; // + }); + + it('should harvest', async function () { + this.enableTimeouts(false); + + ethers.provider.send('evm_increaseTime', [3600 * 24]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOf(); + const tx = await strategy.harvest(100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOf(); + + expect(balanceAfter.gt(balanceBefore)).to.be.true; + }); + + it('should withdraw some for the user', async function () { + this.enableTimeouts(false); + const balanceBefore = await strategy.balanceOf(); + const amountDeposited = await want.balanceOf(WANT_HOLDER); + + const tx0 = await want + .connect(wantHolder) + .approve(vault.address, amountDeposited); + + await tx0.wait(); + + const tx1 = await vault.connect(wantHolder).deposit(amountDeposited); + await tx1.wait(); + + // triple earn to leave close to nothing in vault (5% always stay with earn) + const tx2 = await vault.earn(); + await tx2.wait(); + const tx2b = await vault.earn(); + await tx2b.wait(); + const tx2c = await vault.earn(); + await tx2c.wait(); + + //available in the vault (95%) that can be sent to strat + //const vaultAvailableAfterEarn = await vault.available(); + const vaultAvailableAfterEarn = await want.balanceOf(vault.address); + + const balanceAfterEarn = await strategy.balanceOf(); + + const sdBalance = await vault.balanceOf(WANT_HOLDER); + const pricePerShare = await vault.getPricePerFullShare(); + + const getInWant = (am: BigNumber): any => { + return am + .mul(pricePerShare) + .div(BigNumber.from(1).mul(BigNumber.from(10).pow(18))); + }; + + const withdrawAmount = sdBalance.div(4); + const withdrawAmountWant = getInWant(withdrawAmount); + + const userWantBalanceBefore = await want.balanceOf(WANT_HOLDER); + + const tx3 = await vault.connect(wantHolder).withdraw(withdrawAmount); + await tx3.wait(); + + const balanceAfterWithdraw = await strategy.balanceOf(); + const userWantBalanceAfter = await want.balanceOf(WANT_HOLDER); + + const expectedWithdrawFromStrategy = withdrawAmountWant.sub( + vaultAvailableAfterEarn + ); + + const balanceDifference = balanceAfterEarn.sub(balanceAfterWithdraw); + + // 0.5% user withdrawal paid fees + const withdrawalFee = withdrawAmountWant + .sub(vaultAvailableAfterEarn) + .mul(5) + .div(1000); + + const expectedUserBalanceWantAfter = withdrawAmountWant + .sub(withdrawalFee) + .add(userWantBalanceBefore); + }); + + it('should withdraw all', async function () { + this.enableTimeouts(false); + + const balanceBefore = await strategy.balanceOf(); + const vaultBalanceBefore = await want.balanceOf(vault.address); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, RANDOM); + + await tx1.wait(); + + // replace strat by random strat + const tx2 = await controller.connect(governance).setStrategy(WANT, RANDOM); + + await tx2.wait(); + + const balanceAfter = await strategy.balanceOf(); + const vaultBalanceAfter = await want.balanceOf(vault.address); + + expect(balanceBefore.gt(0)).to.be.true; + expect(balanceAfter.eq(0)).to.be.true; + expect(vaultBalanceAfter.gt(vaultBalanceBefore)).to.be.true; + expect(vaultBalanceBefore.add(balanceBefore).eq(vaultBalanceAfter)).to.be + .true; + }); +}); diff --git a/convex/test/strategySbtc.ts b/convex/test/strategySbtc.ts new file mode 100644 index 0000000..13e9857 --- /dev/null +++ b/convex/test/strategySbtc.ts @@ -0,0 +1,243 @@ +import {ethers, network} from 'hardhat'; +import {expect} from 'chai'; +import {BigNumber} from '@ethersproject/bignumber'; +import {Contract} from '@ethersproject/contracts'; +import {parseEther} from '@ethersproject/units'; +import {JsonRpcSigner} from '@ethersproject/providers'; + +import Controller from '../abis/Controller.json'; +import YVault from '../abis/YVault.json'; +import ERC20 from '../abis/ERC20.json'; + +const CONTROLLER = '0x29D3782825432255041Db2EAfCB7174f5273f08A'; +const VAULT = '0x24129B935AfF071c4f0554882C0D9573F4975fEd'; +const WANT = '0x075b1bb99792c9E1041bA13afEf80C91a1e70fB3'; +const GOVERNANCE = '0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063'; +const WANT_HOLDER = '0x282742940eE0b7ed028Bb48052Bb4922282234dA'; + +const PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A'; +const RANDOM = '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8'; + +const VE_CRV = '0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2'; +const CURVE_VOTER = '0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6'; + +const VEABI = [ + { + name: 'locked', + outputs: [ + {type: 'int128', name: 'amount'}, + {type: 'uint256', name: 'end'} + ], + inputs: [{type: 'address', name: 'arg0'}], + stateMutability: 'view', + type: 'function', + gas: 3359 + } +]; + +describe('ConvexSbtc', function () { + let controller: Contract; + let strategy: Contract; + let vault: Contract; + let want: Contract; + let veCRV: Contract; + let wantHolder: JsonRpcSigner; + let governance: JsonRpcSigner; + let controllerSigner: JsonRpcSigner; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [GOVERNANCE] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [WANT_HOLDER] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [CONTROLLER] + }); + + const Strategy = await ethers.getContractFactory('StrategySbtcConvex'); + + governance = await ethers.provider.getSigner(GOVERNANCE); + wantHolder = await ethers.provider.getSigner(WANT_HOLDER); + controllerSigner = await ethers.provider.getSigner(CONTROLLER); + strategy = await Strategy.deploy(CONTROLLER, PROXY); + controller = await ethers.getContractAt(Controller, CONTROLLER); + vault = await ethers.getContractAt(YVault, VAULT); + want = await ethers.getContractAt(ERC20, WANT); + veCRV = await ethers.getContractAt(VEABI, VE_CRV); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, strategy.address); + + console.log('approveStrategy :', tx1.hash); + await tx1.wait(); + + const tx2 = await controller + .connect(governance) + .setStrategy(WANT, strategy.address); + + console.log('setStrategy :', tx2.hash); + await tx2.wait(); + }); + + it('All funds should be in vault', async function () { + const vaultBalance = await vault.balance(); + const vaultSbtcBalance = await want.balanceOf(vault.address); + expect(vaultBalance.eq(vaultSbtcBalance)).to.be.true; + + console.log({vaultBalance, vaultSbtcBalance}); + }); + + it('should deposit 95% of vault balance in convex sBTC pool', async function () { + this.enableTimeouts(false); + const vaultBalance = await vault.balance(); + const tx = await vault.earn(); + await tx.wait(); + + const balance = await strategy.balanceOf(); + const amountExpected = vaultBalance.mul(95).div(100); + + expect(balance.eq(amountExpected)).to.be.true; + }); + + it('should lock CRV and ensure we have more LP in baseRewardPool', async function () { + this.enableTimeouts(false); + + const {amount: veCRVBalanceBefore} = await veCRV.locked(CURVE_VOTER); + + ethers.provider.send('evm_increaseTime', [3600]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOfPool(); + const tx = await strategy.harvest(100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOfPool(); + + const {amount: veCRVBalanceAfter} = await veCRV.locked(CURVE_VOTER); + const hasLockedCRV = veCRVBalanceAfter.gt(veCRVBalanceBefore); + + expect(hasLockedCRV).to.be.true; // ensure that we lock CRV + expect(balanceAfter.gt(balanceBefore)).to.be.true; // + }); + + it('should harvest', async function () { + this.enableTimeouts(false); + + ethers.provider.send('evm_increaseTime', [3600 * 24]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOf(); + const tx = await strategy.harvest(100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOf(); + + expect(balanceAfter.gt(balanceBefore)).to.be.true; + }); + + it('should withdraw some for the user', async function () { + this.enableTimeouts(false); + const balanceBefore = await strategy.balanceOf(); + const amountDeposited = await want.balanceOf(WANT_HOLDER); + + const tx0 = await want + .connect(wantHolder) + .approve(vault.address, amountDeposited); + + await tx0.wait(); + + const tx1 = await vault.connect(wantHolder).deposit(amountDeposited); + await tx1.wait(); + + // triple earn to leave close to nothing in vault (5% always stay with earn) + const tx2 = await vault.earn(); + await tx2.wait(); + const tx2b = await vault.earn(); + await tx2b.wait(); + const tx2c = await vault.earn(); + await tx2c.wait(); + + //available in the vault (95%) that can be sent to strat + //const vaultAvailableAfterEarn = await vault.available(); + const vaultAvailableAfterEarn = await want.balanceOf(VAULT); + + const balanceAfterEarn = await strategy.balanceOf(); + + const sdBalance = await vault.balanceOf(WANT_HOLDER); + const pricePerShare = await vault.getPricePerFullShare(); + + const getInWant = (am: BigNumber): any => { + return am + .mul(pricePerShare) + .div(BigNumber.from(1).mul(BigNumber.from(10).pow(18))); + }; + + const withdrawAmount = sdBalance.div(4); + const withdrawAmountWant = getInWant(withdrawAmount); + + const userWantBalanceBefore = await want.balanceOf(WANT_HOLDER); + + const tx3 = await vault.connect(wantHolder).withdraw(withdrawAmount); + await tx3.wait(); + + const balanceAfterWithdraw = await strategy.balanceOf(); + const userWantBalanceAfter = await want.balanceOf(WANT_HOLDER); + + const expectedWithdrawFromStrategy = withdrawAmountWant.sub( + vaultAvailableAfterEarn + ); + + const balanceDifference = balanceAfterEarn.sub(balanceAfterWithdraw); + + // 0.5% user withdrawal paid fees + const withdrawalFee = withdrawAmountWant + .sub(vaultAvailableAfterEarn) + .mul(5) + .div(1000); + + const expectedUserBalanceWantAfter = withdrawAmountWant + .sub(withdrawalFee) + .add(userWantBalanceBefore); + + expect(balanceBefore.gt(0)).to.be.true; + expect(balanceAfterEarn.gt(balanceBefore)).to.be.true; + expect(expectedWithdrawFromStrategy.gte(balanceDifference)).to.be.true; + expect(expectedUserBalanceWantAfter.eq(userWantBalanceAfter)).to.be.true; + }); + + it('should withdraw all', async function () { + this.enableTimeouts(false); + + const balanceBefore = await strategy.balanceOf(); + const vaultBalanceBefore = await want.balanceOf(VAULT); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, RANDOM); + + await tx1.wait(); + + // replace strat by random strat + const tx2 = await controller.connect(governance).setStrategy(WANT, RANDOM); + + await tx2.wait(); + + const balanceAfter = await strategy.balanceOf(); + const vaultBalanceAfter = await want.balanceOf(VAULT); + + expect(balanceBefore.gt(0)).to.be.true; + expect(balanceAfter.eq(0)).to.be.true; + expect(vaultBalanceAfter.gt(vaultBalanceBefore)).to.be.true; + expect(vaultBalanceBefore.add(balanceBefore).eq(vaultBalanceAfter)).to.be + .true; + }); +}); diff --git a/convex/test/strategyStEth.ts b/convex/test/strategyStEth.ts new file mode 100644 index 0000000..c2efc0c --- /dev/null +++ b/convex/test/strategyStEth.ts @@ -0,0 +1,292 @@ +import {ethers, network} from 'hardhat'; +import {expect} from 'chai'; +import {BigNumber} from '@ethersproject/bignumber'; +import {Contract} from '@ethersproject/contracts'; +import {parseEther} from '@ethersproject/units'; +import {JsonRpcSigner} from '@ethersproject/providers'; + +import Controller from '../abis/Controller.json'; +import ERC20 from '../abis/ERC20.json'; + +const CONTROLLER = '0x29D3782825432255041Db2EAfCB7174f5273f08A'; +const WANT = '0x06325440D014e39736583c165C2963BA99fAf14E'; +const GOVERNANCE = '0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063'; +const WANT_HOLDER = '0x56c915758Ad3f76Fd287FFF7563ee313142Fb663'; + +const PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A'; +const RANDOM = '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8'; + +const VE_CRV = '0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2'; +const CURVE_VOTER = '0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6'; + +const VEABI = [ + { + name: 'locked', + outputs: [ + {type: 'int128', name: 'amount'}, + {type: 'uint256', name: 'end'} + ], + inputs: [{type: 'address', name: 'arg0'}], + stateMutability: 'view', + type: 'function', + gas: 3359 + } +]; + +describe('ConvexSeth', function () { + let controller: Contract; + let strategy: Contract; + let vault: Contract; + let want: Contract; + let veCRV: Contract; + let wantHolder: JsonRpcSigner; + let governance: JsonRpcSigner; + let controllerSigner: JsonRpcSigner; + let random: JsonRpcSigner; + + before(async function () { + this.enableTimeouts(false); + const [owner] = await ethers.getSigners(); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [GOVERNANCE] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [WANT_HOLDER] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [CONTROLLER] + }); + + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [RANDOM] + }); + + const Vault = await ethers.getContractFactory('Vault'); + const Strategy = await ethers.getContractFactory('StrategyStEthConvex'); + + random = await ethers.provider.getSigner(RANDOM); + governance = await ethers.provider.getSigner(GOVERNANCE); + wantHolder = await ethers.provider.getSigner(WANT_HOLDER); + controllerSigner = await ethers.provider.getSigner(CONTROLLER); + + strategy = await Strategy.deploy(CONTROLLER, PROXY); + vault = await Vault.deploy(WANT, CONTROLLER, GOVERNANCE); + + controller = await ethers.getContractAt(Controller, CONTROLLER); + + want = await ethers.getContractAt(ERC20, WANT); + veCRV = await ethers.getContractAt(VEABI, VE_CRV); + + await ( + await random.sendTransaction({ + to: GOVERNANCE, + value: ethers.utils.parseEther('100') + }) + ).wait(); + + await ( + await random.sendTransaction({ + to: WANT_HOLDER, + value: ethers.utils.parseEther('10') + }) + ).wait(); + + const tx0 = await controller + .connect(governance) + .setVault(WANT, vault.address); + + console.log('setVault :', tx0.hash); + await tx0.wait(); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, strategy.address); + + console.log('approveStrategy :', tx1.hash); + await tx1.wait(); + + const tx2 = await controller + .connect(governance) + .setStrategy(WANT, strategy.address); + + console.log('setStrategy :', tx2.hash); + await tx2.wait(); + + /* + SIMULATE INITIAL DEPOSIT IN THE VAULT + */ + const holderBalance = await want.balanceOf(WANT_HOLDER); + await ( + await want.connect(wantHolder).approve(vault.address, holderBalance) + ).wait(); + + await (await vault.connect(wantHolder).deposit(holderBalance)).wait(); + }); + + it('All funds should be in vault', async function () { + const vaultBalance = await vault.balance(); + const vaulteCRVBalance = await want.balanceOf(vault.address); + + console.log({ + vaultBalance: vaultBalance.toString(), + vaulteCRVBalance: vaulteCRVBalance.toString() + }); + + expect(vaultBalance.eq(vaulteCRVBalance)).to.be.true; + }); + + it('should deposit 95% of vault balance in convex sEth', async function () { + this.enableTimeouts(false); + const vaultBalance = await vault.balance(); + + console.log({ + vaultBalance: vaultBalance.toString() + }); + + const tx = await vault.earn(); + await tx.wait(); + + const balance = await strategy.balanceOf(); + const amountExpected = vaultBalance.mul(95).div(100); + + console.log({ + balance: balance.toString(), + amountExpected: amountExpected.toString() + }); + + expect(balance.eq(amountExpected)).to.be.true; + }); + + it('should lock CRV and ensure we have more LP in baseRewardPool', async function () { + this.enableTimeouts(false); + + const {amount: veCRVBalanceBefore} = await veCRV.locked(CURVE_VOTER); + + ethers.provider.send('evm_increaseTime', [3600]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOfPool(); + const tx = await strategy.harvest(100, 100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOfPool(); + + const {amount: veCRVBalanceAfter} = await veCRV.locked(CURVE_VOTER); + const hasLockedCRV = veCRVBalanceAfter.gt(veCRVBalanceBefore); + + expect(hasLockedCRV).to.be.true; // ensure that we lock CRV + expect(balanceAfter.gt(balanceBefore)).to.be.true; // + }); + + it('should harvest', async function () { + this.enableTimeouts(false); + + ethers.provider.send('evm_increaseTime', [3600 * 24]); + ethers.provider.send('evm_mine', []); + + const balanceBefore = await strategy.balanceOf(); + const tx = await strategy.harvest(100, 100, 100, 100); + await tx.wait(); + const balanceAfter = await strategy.balanceOf(); + + expect(balanceAfter.gt(balanceBefore)).to.be.true; + }); + + it('should withdraw some for the user', async function () { + this.enableTimeouts(false); + const balanceBefore = await strategy.balanceOf(); + const amountDeposited = await want.balanceOf(WANT_HOLDER); + + const tx0 = await want + .connect(wantHolder) + .approve(vault.address, amountDeposited); + + await tx0.wait(); + + const tx1 = await vault.connect(wantHolder).deposit(amountDeposited); + await tx1.wait(); + + // triple earn to leave close to nothing in vault (5% always stay with earn) + const tx2 = await vault.earn(); + await tx2.wait(); + const tx2b = await vault.earn(); + await tx2b.wait(); + const tx2c = await vault.earn(); + await tx2c.wait(); + + //available in the vault (95%) that can be sent to strat + //const vaultAvailableAfterEarn = await vault.available(); + const vaultAvailableAfterEarn = await want.balanceOf(vault.address); + + const balanceAfterEarn = await strategy.balanceOf(); + + const sdBalance = await vault.balanceOf(WANT_HOLDER); + const pricePerShare = await vault.getPricePerFullShare(); + + const getInWant = (am: BigNumber): any => { + return am + .mul(pricePerShare) + .div(BigNumber.from(1).mul(BigNumber.from(10).pow(18))); + }; + + const withdrawAmount = sdBalance.div(4); + const withdrawAmountWant = getInWant(withdrawAmount); + + const userWantBalanceBefore = await want.balanceOf(WANT_HOLDER); + + const tx3 = await vault.connect(wantHolder).withdraw(withdrawAmount); + await tx3.wait(); + + const balanceAfterWithdraw = await strategy.balanceOf(); + const userWantBalanceAfter = await want.balanceOf(WANT_HOLDER); + + const expectedWithdrawFromStrategy = withdrawAmountWant.sub( + vaultAvailableAfterEarn + ); + + const balanceDifference = balanceAfterEarn.sub(balanceAfterWithdraw); + + // 0.5% user withdrawal paid fees + const withdrawalFee = withdrawAmountWant + .sub(vaultAvailableAfterEarn) + .mul(5) + .div(1000); + + const expectedUserBalanceWantAfter = withdrawAmountWant + .sub(withdrawalFee) + .add(userWantBalanceBefore); + }); + + it('should withdraw all', async function () { + this.enableTimeouts(false); + + const balanceBefore = await strategy.balanceOf(); + const vaultBalanceBefore = await want.balanceOf(vault.address); + + const tx1 = await controller + .connect(governance) + .approveStrategy(WANT, RANDOM); + + await tx1.wait(); + + // replace strat by random strat + const tx2 = await controller.connect(governance).setStrategy(WANT, RANDOM); + + await tx2.wait(); + + const balanceAfter = await strategy.balanceOf(); + const vaultBalanceAfter = await want.balanceOf(vault.address); + + expect(balanceBefore.gt(0)).to.be.true; + expect(balanceAfter.eq(0)).to.be.true; + expect(vaultBalanceAfter.gt(vaultBalanceBefore)).to.be.true; + expect(vaultBalanceBefore.add(balanceBefore).eq(vaultBalanceAfter)).to.be + .true; + }); +}); diff --git a/convex/tsconfig.json b/convex/tsconfig.json new file mode 100644 index 0000000..ea7d94d --- /dev/null +++ b/convex/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "resolveJsonModule": true + }, + "include": ["./scripts", "./test", "test/strategySbtc.ts"], + "files": ["./hardhat.config.ts"] +} diff --git a/harmony/.gitignore b/harmony/.gitignore new file mode 100644 index 0000000..ce21ffc --- /dev/null +++ b/harmony/.gitignore @@ -0,0 +1,4 @@ +.env +artifacts +cache +node_modules \ No newline at end of file diff --git a/harmony/.prettierrc b/harmony/.prettierrc new file mode 100644 index 0000000..6507f8c --- /dev/null +++ b/harmony/.prettierrc @@ -0,0 +1,21 @@ +{ + "overrides": [ + { + "files": "*.sol", + "options": { + "printWidth": 80, + "tabWidth": 2, + "useTabs": true, + "singleQuote": false, + "bracketSpacing": true, + "explicitTypes": "always" + } + } + ], + "printWidth": 80, + "singleQuote": false, + "bracketSpacing": true, + "explicitTypes": "always", + "trailingComma": "none", + "arrowParens": "avoid" +} diff --git a/harmony/README.md b/harmony/README.md new file mode 100644 index 0000000..6646d92 --- /dev/null +++ b/harmony/README.md @@ -0,0 +1,11 @@ +# sd-harmony +harmony deployment + +1) Install dependencies +`yarn` + +2) Compile contracts +`yarn compile` + +3) Test +`yarn test` diff --git a/harmony/contracts/Controller.sol b/harmony/contracts/Controller.sol new file mode 100644 index 0000000..88928f7 --- /dev/null +++ b/harmony/contracts/Controller.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +interface IStrategy { + function want() external view returns (address); + + function deposit() external; + + function withdraw(address) external; + + function withdraw(uint256) external; + + function withdrawAll() external returns (uint256); + + function balanceOf() external view returns (uint256); +} + +contract Controller { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public governance; + address public strategist; + + address public rewards; + mapping(address => address) public vaults; + mapping(address => address) public strategies; + + mapping(address => mapping(address => bool)) public approvedStrategies; + + uint256 public constant max = 10000; + + event SetStrategy(address indexed asset, address indexed strategy); + event ApproveStrategy(address indexed asset, address indexed strategy); + event SetVault(address indexed asset, address indexed vault); + + constructor(address _rewards) public { + governance = msg.sender; + strategist = msg.sender; + rewards = _rewards; + } + + function setRewards(address _rewards) public { + require(msg.sender == governance, '!governance'); + rewards = _rewards; + } + + function setStrategist(address _strategist) public { + require(msg.sender == governance, '!governance'); + strategist = _strategist; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, '!governance'); + governance = _governance; + } + + function setVault(address _token, address _vault) public { + require( + msg.sender == strategist || msg.sender == governance, + '!strategist' + ); + require(vaults[_token] == address(0), 'vault'); + vaults[_token] = _vault; + emit SetVault(_token, _vault); + } + + function approveStrategy(address _token, address _strategy) public { + require(msg.sender == governance, '!governance'); + approvedStrategies[_token][_strategy] = true; + emit ApproveStrategy(_token, _strategy); + } + + function revokeStrategy(address _token, address _strategy) public { + require(msg.sender == governance, '!governance'); + approvedStrategies[_token][_strategy] = false; + } + + function setStrategy(address _token, address _strategy) public { + require( + msg.sender == strategist || msg.sender == governance, + '!strategist' + ); + require(approvedStrategies[_token][_strategy] == true, '!approved'); + + address _current = strategies[_token]; + if (_current != address(0)) { + IStrategy(_current).withdrawAll(); + } + strategies[_token] = _strategy; + emit SetStrategy(_token, _strategy); + } + + function earn(address _token, uint256 _amount) public { + address _strategy = strategies[_token]; + IERC20(_token).safeTransfer(_strategy, _amount); + IStrategy(_strategy).deposit(); + } + + function balanceOf(address _token) external view returns (uint256) { + return IStrategy(strategies[_token]).balanceOf(); + } + + function withdrawAll(address _token) public { + require( + msg.sender == strategist || msg.sender == governance, + '!strategist' + ); + IStrategy(strategies[_token]).withdrawAll(); + } + + function inCaseTokensGetStuck(address _token, uint256 _amount) public { + require( + msg.sender == strategist || msg.sender == governance, + '!governance' + ); + IERC20(_token).safeTransfer(msg.sender, _amount); + } + + function inCaseStrategyTokenGetStuck(address _strategy, address _token) + public + { + require( + msg.sender == strategist || msg.sender == governance, + '!governance' + ); + IStrategy(_strategy).withdraw(_token); + } + + function withdraw(address _token, uint256 _amount) public { + require(msg.sender == vaults[_token], '!vault'); + IStrategy(strategies[_token]).withdraw(_amount); + } +} \ No newline at end of file diff --git a/harmony/contracts/MultiRewards.sol b/harmony/contracts/MultiRewards.sol new file mode 100644 index 0000000..d1c0caa --- /dev/null +++ b/harmony/contracts/MultiRewards.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/math/Math.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +contract MultiRewards is ReentrancyGuard, Pausable, Ownable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /* ========== STATE VARIABLES ========== */ + + struct Reward { + address rewardsDistributor; + uint256 rewardsDuration; + uint256 periodFinish; + uint256 rewardRate; + uint256 lastUpdateTime; + uint256 rewardPerTokenStored; + } + IERC20 public stakingToken; + mapping(address => Reward) public rewardData; + address[] public rewardTokens; + + // user -> reward token -> amount + mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid; + mapping(address => mapping(address => uint256)) public rewards; + + uint256 private _totalSupply; + mapping(address => uint256) private _balances; + + /* ========== CONSTRUCTOR ========== */ + + constructor(address _stakingToken) public { + stakingToken = IERC20(_stakingToken); + } + + function addReward( + address _rewardsToken, + address _rewardsDistributor, + uint256 _rewardsDuration + ) public onlyOwner { + require(rewardData[_rewardsToken].rewardsDuration == 0); + rewardTokens.push(_rewardsToken); + rewardData[_rewardsToken].rewardsDistributor = _rewardsDistributor; + rewardData[_rewardsToken].rewardsDuration = _rewardsDuration; + } + + /* ========== VIEWS ========== */ + + function totalSupply() external view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) external view returns (uint256) { + return _balances[account]; + } + + function lastTimeRewardApplicable(address _rewardsToken) + public + view + returns (uint256) + { + return Math.min(block.timestamp, rewardData[_rewardsToken].periodFinish); + } + + function rewardPerToken(address _rewardsToken) public view returns (uint256) { + if (_totalSupply == 0) { + return rewardData[_rewardsToken].rewardPerTokenStored; + } + return + rewardData[_rewardsToken].rewardPerTokenStored.add( + lastTimeRewardApplicable(_rewardsToken) + .sub(rewardData[_rewardsToken].lastUpdateTime) + .mul(rewardData[_rewardsToken].rewardRate) + .mul(1e18) + .div(_totalSupply) + ); + } + + function earned(address account, address _rewardsToken) + public + view + returns (uint256) + { + return + _balances[account] + .mul( + rewardPerToken(_rewardsToken).sub( + userRewardPerTokenPaid[account][_rewardsToken] + ) + ) + .div(1e18) + .add(rewards[account][_rewardsToken]); + } + + function getRewardForDuration(address _rewardsToken) + external + view + returns (uint256) + { + return + rewardData[_rewardsToken].rewardRate.mul( + rewardData[_rewardsToken].rewardsDuration + ); + } + + /* ========== MUTATIVE FUNCTIONS ========== */ + + function setRewardsDistributor( + address _rewardsToken, + address _rewardsDistributor + ) external onlyOwner { + rewardData[_rewardsToken].rewardsDistributor = _rewardsDistributor; + } + + function _stake(uint256 amount, address account) + internal + nonReentrant + whenNotPaused + updateReward(account) + { + require(amount > 0, "Cannot stake 0"); + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + stakingToken.safeTransferFrom(msg.sender, address(this), amount); + emit Staked(account, amount); + } + + function stakeFor(address account, uint256 amount) external { + _stake(amount, account); + } + + function _withdraw(uint256 amount, address account) + internal + nonReentrant + updateReward(account) + { + require(amount > 0, "Cannot withdraw 0"); + _totalSupply = _totalSupply.sub(amount); + _balances[account] = _balances[account].sub(amount); + stakingToken.safeTransfer(msg.sender, amount); + emit Withdrawn(account, amount); + } + + function withdraw(uint256 amount, address account) external { + _withdraw(amount, msg.sender); + } + + function withdrawFor(address account, uint256 amount) external { + require(tx.origin == account, "withdrawFor: account != tx.origin"); + _withdraw(amount, account); + } + + function getReward() public nonReentrant updateReward(msg.sender) { + for (uint256 i; i < rewardTokens.length; i++) { + address _rewardsToken = rewardTokens[i]; + uint256 reward = rewards[msg.sender][_rewardsToken]; + if (reward > 0) { + rewards[msg.sender][_rewardsToken] = 0; + IERC20(_rewardsToken).safeTransfer(msg.sender, reward); + emit RewardPaid(msg.sender, _rewardsToken, reward); + } + } + } + + function getRewardFor(address account) + public + nonReentrant + updateReward(account) + { + for (uint256 i; i < rewardTokens.length; i++) { + address _rewardsToken = rewardTokens[i]; + uint256 reward = rewards[account][_rewardsToken]; + if (reward > 0) { + rewards[account][_rewardsToken] = 0; + IERC20(_rewardsToken).safeTransfer(account, reward); + emit RewardPaid(account, _rewardsToken, reward); + } + } + } + + function exit() external { + _withdraw(_balances[msg.sender], msg.sender); + getReward(); + } + + /* ========== RESTRICTED FUNCTIONS ========== */ + + function notifyRewardAmount(address _rewardsToken, uint256 reward) + external + updateReward(address(0)) + { + require(rewardData[_rewardsToken].rewardsDistributor == msg.sender); + // handle the transfer of reward tokens via `transferFrom` to reduce the number + // of transactions required and ensure correctness of the reward amount + IERC20(_rewardsToken).safeTransferFrom(msg.sender, address(this), reward); + + if (block.timestamp >= rewardData[_rewardsToken].periodFinish) { + rewardData[_rewardsToken].rewardRate = reward.div( + rewardData[_rewardsToken].rewardsDuration + ); + } else { + uint256 remaining = rewardData[_rewardsToken].periodFinish.sub( + block.timestamp + ); + uint256 leftover = remaining.mul(rewardData[_rewardsToken].rewardRate); + rewardData[_rewardsToken].rewardRate = reward.add(leftover).div( + rewardData[_rewardsToken].rewardsDuration + ); + } + + rewardData[_rewardsToken].lastUpdateTime = block.timestamp; + rewardData[_rewardsToken].periodFinish = block.timestamp.add( + rewardData[_rewardsToken].rewardsDuration + ); + emit RewardAdded(reward); + } + + // Added to support recovering LP Rewards from other systems such as BAL to be distributed to holders + function recoverERC20(address tokenAddress, uint256 tokenAmount) + external + onlyOwner + { + require( + tokenAddress != address(stakingToken), + "Cannot withdraw staking token" + ); + require( + rewardData[tokenAddress].lastUpdateTime == 0, + "Cannot withdraw reward token" + ); + IERC20(tokenAddress).safeTransfer(owner(), tokenAmount); + emit Recovered(tokenAddress, tokenAmount); + } + + function setRewardsDuration(address _rewardsToken, uint256 _rewardsDuration) + external + { + require( + block.timestamp > rewardData[_rewardsToken].periodFinish, + "Reward period still active" + ); + require(rewardData[_rewardsToken].rewardsDistributor == msg.sender); + require(_rewardsDuration > 0, "Reward duration must be non-zero"); + rewardData[_rewardsToken].rewardsDuration = _rewardsDuration; + emit RewardsDurationUpdated( + _rewardsToken, + rewardData[_rewardsToken].rewardsDuration + ); + } + + /* ========== MODIFIERS ========== */ + + modifier updateReward(address account) { + for (uint256 i; i < rewardTokens.length; i++) { + address token = rewardTokens[i]; + rewardData[token].rewardPerTokenStored = rewardPerToken(token); + rewardData[token].lastUpdateTime = lastTimeRewardApplicable(token); + if (account != address(0)) { + rewards[account][token] = earned(account, token); + userRewardPerTokenPaid[account][token] = rewardData[token] + .rewardPerTokenStored; + } + } + _; + } + + /* ========== EVENTS ========== */ + + event RewardAdded(uint256 reward); + event Staked(address indexed user, uint256 amount); + event Withdrawn(address indexed user, uint256 amount); + event RewardPaid( + address indexed user, + address indexed rewardsToken, + uint256 reward + ); + event RewardsDurationUpdated(address token, uint256 newDuration); + event Recovered(address token, uint256 amount); +} diff --git a/harmony/contracts/Vault.sol b/harmony/contracts/Vault.sol new file mode 100644 index 0000000..268fe41 --- /dev/null +++ b/harmony/contracts/Vault.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./interfaces/IMultiRewards.sol"; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +contract Vault is ERC20 { + using SafeERC20 for ERC20; + using Address for address; + using SafeMath for uint256; + + ERC20 public token; + + uint256 public min = 10000; + uint256 public constant max = 10000; + + address public governance; + address public controller; + address public gauge; + event Deposit( + address indexed _from, + uint256 _shares, + uint256 _amount, + uint256 pps + ); + + event Withdraw( + address indexed _from, + uint256 _shares, + uint256 _amount, + uint256 pps + ); + + event Earn(address indexed _from, uint256 _amount); + + constructor( + address _token, + address _controller, + address _governance, + string memory _namePrefix, + string memory _symbolPrefix + ) + public + ERC20( + string(abi.encodePacked(_namePrefix, ERC20(_token).name())), + string(abi.encodePacked(_symbolPrefix, ERC20(_token).symbol())) + ) + { + token = ERC20(_token); + controller = _controller; + governance = _governance; + } + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + function decimals() public view virtual override returns (uint8) { + return token.decimals(); + } + + function balance() public view returns (uint256) { + return + token.balanceOf(address(this)).add( + IController(controller).balanceOf(address(token)) + ); + } + + function setMin(uint256 _min) external onlyGovernance { + min = _min; + } + + function setGovernance(address _governance) public onlyGovernance { + governance = _governance; + } + + function setController(address _controller) public onlyGovernance { + controller = _controller; + } + + function setGauge(address _gauge) public onlyGovernance { + gauge = _gauge; + } + + function available() public view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() public { + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + emit Earn(msg.sender, _bal); + } + + function depositAndEarn(uint256 _amount) external { + deposit(_amount); + earn(); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint256 _amount) public { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + + _amount = _after.sub(_before); + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(address(this), shares); + IERC20(address(this)).approve(gauge, shares); + IMultiRewards(gauge).stakeFor(msg.sender, shares); + emit Deposit(msg.sender, shares, _amount, getPricePerFullShare()); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + function withdraw(uint256 _shares) public { + uint256 userTotalShares = IMultiRewards(gauge).balanceOf(msg.sender); + require(_shares <= userTotalShares, "Not enough staked"); + IMultiRewards(gauge).withdrawFor(msg.sender, _shares); + IMultiRewards(gauge).getRewardFor(msg.sender); + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(address(this), _shares); + + uint256 b = token.balanceOf(address(this)); + + if (b < r) { + uint256 _withdraw = r.sub(b); + IController(controller).withdraw(address(token), _withdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + + token.safeTransfer(msg.sender, r); + emit Withdraw(msg.sender, _shares, r, getPricePerFullShare()); + } + + function getPricePerFullShare() public view returns (uint256) { + return totalSupply() == 0 ? 1e18 : balance().mul(1e18).div(totalSupply()); + } +} diff --git a/harmony/contracts/interfaces/IController.sol b/harmony/contracts/interfaces/IController.sol new file mode 100644 index 0000000..aa5ea54 --- /dev/null +++ b/harmony/contracts/interfaces/IController.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} \ No newline at end of file diff --git a/harmony/contracts/interfaces/ICurveGauge.sol b/harmony/contracts/interfaces/ICurveGauge.sol new file mode 100644 index 0000000..4878767 --- /dev/null +++ b/harmony/contracts/interfaces/ICurveGauge.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +interface ICurveGauge { + function deposit(uint256) external; + + function balanceOf(address) external view returns (uint256); + + function withdraw(uint256) external; + + function claim_rewards() external; +} \ No newline at end of file diff --git a/harmony/contracts/interfaces/ICurvePool.sol b/harmony/contracts/interfaces/ICurvePool.sol new file mode 100644 index 0000000..8edda4f --- /dev/null +++ b/harmony/contracts/interfaces/ICurvePool.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +interface ICurvePool { + function add_liquidity( + uint256[3] calldata amounts, + uint256 min_mint_amount + ) external returns (uint256); +} \ No newline at end of file diff --git a/harmony/contracts/interfaces/IMultiRewards.sol b/harmony/contracts/interfaces/IMultiRewards.sol new file mode 100644 index 0000000..35183b3 --- /dev/null +++ b/harmony/contracts/interfaces/IMultiRewards.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +interface IMultiRewards { + function balanceOf(address) external returns (uint256); + + function stakeFor(address, uint256) external; + + function withdrawFor(address, uint256) external; + + function notifyRewardAmount(address, uint256) external; + + function getRewardFor(address) external; +} diff --git a/harmony/contracts/interfaces/IUniswapRouter.sol b/harmony/contracts/interfaces/IUniswapRouter.sol new file mode 100644 index 0000000..4c68df4 --- /dev/null +++ b/harmony/contracts/interfaces/IUniswapRouter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2; + +interface IUniswapRouter { + function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); + + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); +} \ No newline at end of file diff --git a/harmony/contracts/strategies/StrategyCurveAave.sol b/harmony/contracts/strategies/StrategyCurveAave.sol new file mode 100644 index 0000000..b59bc4c --- /dev/null +++ b/harmony/contracts/strategies/StrategyCurveAave.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +import "../interfaces/IUniswapRouter.sol"; +import "../interfaces/ICurvePool.sol"; +import "../interfaces/ICurveGauge.sol"; +import "../interfaces/IController.sol"; + +contract StrategyCurveAave { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address(0xC5cfaDA84E902aD92DD40194f0883ad49639b023); + + address public constant usdc = address(0x985458E523dB3d53125813eD68c274899e9DfAb4); + + address public constant wone = address(0xcF664087a5bB0237a0BAd6742852ec6c8d69A27a); + + address public constant crv = address(0x352cd428EFd6F31B5cae636928b7B84149cF369F); + + address public constant pool = address(0xC5cfaDA84E902aD92DD40194f0883ad49639b023); + + address public gauge = address(0xbF7E49483881C76487b0989CD7d9A8239B20CA41); + + address public constant sushiRouter = address(0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506); + + uint256 public performanceFee = 1500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCurveAave"; + } + + function setGauge(address _gauge) external onlyGovernance { + gauge = _gauge; + } + + function setStrategist(address _strategist) external onlyGovernance { + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external onlyGovernance { + performanceFee = _performanceFee; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(gauge, _want); + ICurveGauge(gauge).deposit(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external onlyController returns (uint256 balance) { + require(want != address(_asset), "want"); + require(usdc != address(_asset), "usdc"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal { + ICurveGauge(gauge).withdraw(_amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = balanceOfWant(); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + uint256 _before = balanceOf(); + _withdrawSome(balanceOfPool()); + require(_before == balanceOf(), "!slippage"); + } + + // slippageCRV = 100 for 1% max slippage + function _swapOnSushi(address[] memory path, uint256 _amount, uint256 _maxSlippage) internal returns (uint256) { + IERC20(path[0]).safeApprove(sushiRouter, 0); + IERC20(path[0]).safeApprove(sushiRouter, _amount); + + uint256[] memory amounts = IUniswapRouter(sushiRouter).getAmountsOut(_amount, path); + + uint256 minAmount = amounts[path.length - 1].mul(10000 - _maxSlippage).div(10000); + + uint256[] memory outputs = IUniswapRouter(sushiRouter).swapExactTokensForTokens( + _amount, + minAmount, + path, + address(this), + now.add(1800) + ); + + return outputs[1]; + } + + function harvest( + address[] memory _wone_usdc_path, + uint256 maxSlippageWone, + address[] memory _crv_usdc_path, + uint256 maxSlippageCrv + ) public { + require(msg.sender == strategist || msg.sender == governance, "!authorized"); + + ICurveGauge(gauge).claim_rewards(); + + uint256 _wone = IERC20(wone).balanceOf(address(this)); + uint256 _crv = IERC20(crv).balanceOf(address(this)); + + if (_wone > 0) { + //address[] memory wone_usdc_path = new address[](2); + //wone_usdc_path[0] = wone; + //wone_usdc_path[1] = usdc; + address[] memory wone_usdc_path = _wone_usdc_path; + + _swapOnSushi(wone_usdc_path, _wone, maxSlippageWone); + } + + if (_crv > 0) { + // address[] memory crv_usdc_path = new address[](2); + // crv_usdc_path[0] = crv; + // crv_usdc_path[1] = usdc; + address[] memory crv_usdc_path = _crv_usdc_path; + + _swapOnSushi(crv_usdc_path, _crv, maxSlippageCrv); + } + + uint256 _usdc = IERC20(usdc).balanceOf(address(this)); + + if (_usdc > 0) { + IERC20(usdc).approve(pool, _usdc); + ICurvePool(pool).add_liquidity([0, _usdc, 0], 0); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return ICurveGauge(gauge).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} \ No newline at end of file diff --git a/harmony/hardhat.config.ts b/harmony/hardhat.config.ts new file mode 100644 index 0000000..43a01c9 --- /dev/null +++ b/harmony/hardhat.config.ts @@ -0,0 +1,40 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomiclabs/hardhat-waffle"; +import "@nomiclabs/hardhat-etherscan"; +import "@nomiclabs/hardhat-vyper"; + +import "hardhat-deploy"; +import "hardhat-deploy-ethers"; +import "./tasks/global"; + +require("dotenv").config(); + +const TESTING_DEPLOYER_PKEY = process.env.TESTING_DEPLOYER_PKEY; + +export default { + defaultNetwork: "hardhat", + networks: { + harmony: { + url: `https://api.harmony.one`, + accounts: [`0x${TESTING_DEPLOYER_PKEY}`] + }, + }, + namedAccounts: { + deployer: 0, + }, + vyper: { + version: "0.2.7", + }, + solidity: { + compilers: [{ version: "0.8.0" }, { version: "0.7.4" }, { version: "0.6.12" }, { version: "0.5.17" }], + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + etherscan: { + apiKey: process.env.ETHERSCAN_KEY, + }, +} as HardhatUserConfig; \ No newline at end of file diff --git a/harmony/package.json b/harmony/package.json new file mode 100644 index 0000000..798d667 --- /dev/null +++ b/harmony/package.json @@ -0,0 +1,38 @@ +{ + "name": "sd-harmony", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "@nomiclabs/hardhat-etherscan": "2.1.3", + "@nomiclabs/hardhat-waffle": "^2.0.0", + "@types/chai": "^4.2.17", + "@types/mocha": "^8.2.2", + "@types/node": "^15.0.1", + "chai": "^4.3.4", + "ethereum-waffle": "^3.0.0", + "ethers": "^5.4.0", + "hardhat": "^2.6.6", + "hardhat-abi-exporter": "^2.2.1", + "prettier-plugin-solidity": "^1.0.0-beta.10", + "ts-node": "^9.1.1", + "typescript": "^4.2.4" + }, + "dependencies": { + "@0xsequence/erc-1155": "^3.0.4", + "@chainlink/contracts": "^0.1.7", + "@ethersproject/units": "^5.3.0", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-vyper": "^2.0.1", + "@openzeppelin/contracts": "3.4.0", + "aigle": "^1.14.1", + "dotenv": "^8.2.0", + "hardhat-deploy": "^0.8.11", + "hardhat-deploy-ethers": "^0.3.0-beta.10", + "prettier": "^2.3.0" + }, + "scripts": { + "compile":"npx hardhat compile", + "test":"npx hardhat test" + } +} \ No newline at end of file diff --git a/harmony/tsconfig.json b/harmony/tsconfig.json new file mode 100644 index 0000000..ce64b4e --- /dev/null +++ b/harmony/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "resolveJsonModule": true + }, + "include": ["./scripts", "./test"], + "files": ["./hardhat.config.ts"] + } \ No newline at end of file diff --git a/protocol/.gitattributes b/protocol/.gitattributes new file mode 100644 index 0000000..adb20fe --- /dev/null +++ b/protocol/.gitattributes @@ -0,0 +1,2 @@ +*.sol linguist-language=Solidity +*.vy linguist-language=Python diff --git a/protocol/.gitignore b/protocol/.gitignore new file mode 100644 index 0000000..aec5997 --- /dev/null +++ b/protocol/.gitignore @@ -0,0 +1,12 @@ +__pycache__ +.history +.secret +.infuraID +.hypothesis/ +build/ +reports/ +node_modules +bin/ +mnemonic.txt +merkle +yarn-error.log diff --git a/protocol/README.md b/protocol/README.md new file mode 100644 index 0000000..c9ec08d --- /dev/null +++ b/protocol/README.md @@ -0,0 +1,173 @@ +# protocol + +> Contracts managing Stake DAO + +## Diffcheckers +1. SushiBar-Sanctuary: https://www.diffchecker.com/LNghRNOO +2. Synthetix Rewards Contract - Sanctuary (for newly added notifyRewardAmount() in Sanctuary.sol): https://etherscan.io/address/0xdcb6a51ea3ca5d3fd898fd6564757c7aaec3ca92#code +3. LPGenesisPool-StakedaoNFTPalace: https://www.diffchecker.com/NIJG4nn3 + + +## Architecture + +![Stake Dao Architecture](./assets/Architecture.jpg) + +## Project Structure + +Refer to [eth-brownie documentation](https://eth-brownie.readthedocs.io/en/stable/structure.html). + +## Requirements + +### Install + +- [Python3](https://www.python.org/download/releases/3.0/) +- [eth-brownie](https://eth-brownie.readthedocs.io/en/stable/install.html) **strictly `1.12.0` or above** +- Node.js 10.x development environment (for Ganache). +- Ganache (v6.12.1) + +### Run + +```bash +git clone https://github.com/StakeDAO/protocol +cd protocol +yarn install --lock-file +pip install -r requirements-dev.txt +``` + +## Compiling Contracts + +Run the following command to compile the contracts. + +``` +brownie compile --all +``` + +## Deploying Contracts + +### DEV + +The deployment scripts can be found in the [scripts/deploy/dev](./scripts/deploy/dev) folder. + +To run the deployment script, run: + +``` +python scripts/deploy/dev/deploy.py +``` + +### PROD + +The deployment scripts can be found in the [scripts/deploy/prod](./scripts/deploy/prod) folder. + +To run the deployment script, follow the steps: + +1. Step up the `stakedao-deployer-rug-pull` account: + +``` +brownie accounts new stakedao-deployer-rug-pull +``` + +2. Make sure that all the parameters have been setup in `config.json` file: + +3. Make sure you have enough ETH (maybe 2 ETH) in the deployer account. + +4. Run the script to start deployment: + +``` +python scripts/deploy/prod/deploy.py +``` + +## Funding Contributors + +Run the following scripts to fund contributors. + +``` +brownie run scripts/contributors/fund.py +``` + +## Running Tests + +Run tests: + +```bash +brownie test -s +``` + +Run tests with coverage: + +```bash +brownie test -s --coverage +``` + +### Polygon + +1. Setup network: + +```bash +brownie networks add development polygon-fork cmd=ganache-cli host=http://127.0.0.1 fork=https://rpc-mainnet.maticvigil.com/ accounts=10 mnemonic=brownie port=8545 +``` + +2. Run Tests +```bash +brownie test tests/polygon --network polygon-fork +``` + + +## Formatting + +Check linter rules for `*.json` and `*.sol` files: + +```bash +yarn lint:check +``` + +Fix linter errors for `*.json` and `*.sol` files: + +```bash +yarn lint:fix +``` + +Check linter rules for `*.py` files: + +```bash +black . --check --config black-config.toml +``` + +Fix linter errors for `*.py` files: + +```bash +black . --config black-config.toml +``` + +## Steps to run dev deployment + +All the prod deployment scripts are in [prod folder](./scripts/prod). + +1. Add a `mnemonic.txt` file in the root folder of the project. + +2. Start a network remote/local (eg. local mainnet fork). + +``` +ganache-cli --port 8545 --gasLimit 12000000 --accounts 10 --hardfork istanbul --mnemonic brownie --fork https://mainnet.infura.io/v3/4b7b9649ead646dcb583a25f19831bef +``` + +3. Run the following script in the terminal: `python scripts/deploy/dev/deploy.py YOUR_NETWORK` + +## Steps to run prod deployment + +1. Setup a `stakedao-deployer-rug-pull` account: + +``` +brownie accounts new stakedao-deployer-rug-pull +``` + +2. Run the following script in the terminal: `python scripts/deploy/prod/deploy.py` + +## Steps to generate airdrop merkle json + +Run the following script: + +``` +brownie run scripts/airdrop/airdrop-json-generator.py +``` + +**The script will output the number of airdrop addresses and the final merkle root hash.** diff --git a/protocol/assets/Architecture.jpg b/protocol/assets/Architecture.jpg new file mode 100644 index 0000000..6a63ebf Binary files /dev/null and b/protocol/assets/Architecture.jpg differ diff --git a/protocol/brownie-config.yaml b/protocol/brownie-config.yaml new file mode 100644 index 0000000..75ae4ae --- /dev/null +++ b/protocol/brownie-config.yaml @@ -0,0 +1,42 @@ +# use Ganache's forked mainnet mode as the default network +networks: + default: mainnet-fork + development: + cmd_settings: + accounts: 100 + mainnet-fork: + host: https://mainnet.infura.io/v3/ + cmd_settings: + unlock: 0xeAa9fFaf11cFADde18645875151fBF85ed6C8C98 + mainnet: + host: https://mainnet.infura.io/v3/ + gas_limit: auto + gas_price: auto + reverting_tx_gas_limit: false + cmd_settings: + unlock: 0xeAa9fFaf11cFADde18645875151fBF85ed6C8C98 + +# automatically fetch contract sources from Etherscan +autofetch_sources: True + +# require OpenZepplin Contracts +dependencies: + - OpenZeppelin/openzeppelin-contracts@2.5.1 + - OpenZeppelin/openzeppelin-contracts@3.1.0 + - 0xsequence/multi-token-standard@0.8.13 + +# path remapping to support OpenZepplin imports with NPM-style path +compiler: + solc: + remappings: + - "@openzeppelinV2=OpenZeppelin/openzeppelin-contracts@2.5.1" + - "@openzeppelinV3=OpenZeppelin/openzeppelin-contracts@3.1.0" + - "@multi-token-standard=0xsequence/multi-token-standard@0.8.13" + +reports: + exclude_paths: + - contracts/test/Token.sol + exclude_contracts: + - SafeMath + +dev_deployment_artifacts: True diff --git a/protocol/commitlint.config.js b/protocol/commitlint.config.js new file mode 100644 index 0000000..e2b5db2 --- /dev/null +++ b/protocol/commitlint.config.js @@ -0,0 +1 @@ +module.exports = {extends: ["@commitlint/config-conventional"]}; diff --git a/protocol/config.json b/protocol/config.json new file mode 100644 index 0000000..e23cc60 --- /dev/null +++ b/protocol/config.json @@ -0,0 +1,130 @@ +{ + "polygon": { + "strategies": { + "StrategyCurveAm3Crv": { + "vaultToken": "0xE7a24EF0C5e95Ffb0f6684b813A78F2a3AD7D171", + "gaugeAddress": "0xe381C25de995d62b453aF8B931aAc84fcCaa7A62" + } + } + }, + "dev": { + "UNISWAP_EARLY_LIQUIDITY": "0x13BB257fD618534cc9a0aBBaEE2BD76B0CF11344", + "POOL_LIQUIDITY": 100000000000000000000000, + "UNISWAP_POOL": "0xc465C0a16228Ef6fE1bF29C04Fdb04bb797fd537", + "SUSHISWAP_POOL": "0x22DEF8cF4E481417cb014D9dc64975BA12E3a184", + "AIRDROP_MERKLE_ROOT": "0x43ce90b1f16a7466087f2d22fe1cea4ff420052019b24d0a7b96a9d235caf910", + "ZKSYNC_AIRDROP_MERKLE_ROOT": "0xee170cda08fbcc441aed93461d15faefdb4fbda22462bd031d2ac35dfc6c4fd9", + "SDT_PER_BLOCK": 5000000000000000000, + "AIR_DROP_AMOUNT": 1500000000000000000000000, + "DEAD_EOA": "0x000000000000000000000000000000000000dead", + "CONTRIBUTORS_AMOUNT": 40000000000000000000000000, + "TOTAL_SDT_CIRCULATION": 100000000000000000000000000, + "EPOCH_DURATION": 86400, + "REWARD_REDUCTION_PER_EPOCH": 1000, + "CLAIM_START_TIME": 32400, + "GRACE_PERIOD": 172800, + "TIMELOCK_DELAY": 28800, + "VESTING_START_TIME": 32400, + "VESTING_END_TIME": 63136800, + "VESTING_FUND_ADMINS": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ], + "MASTERCHEF_BONUS_END_BLOCK": 91215, + "VESTING_TOKEN": "0x8e92B1322C4ec3887b81C9D6552136367d2619C6", + "DEFAULT_DEPLOYER_ACCOUNT": "0x93A62dA5a14C80f265DAbC077fCEE437B1a0Efde", + "GNOSISSAFE_MASTERCOPY": "0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F", + "GNOSISSAFE_PROXY_FACTORY": "0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B", + "GNOSIS_SAFE_PROXY": "0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063", + "DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "EURS_CRV": "0x194eBd173F6cDacE046C53eACcE9B953F28411d1", + "BADGER_HUNT_OWNER": "0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063", + "ESCROW": "0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063", + "SDT": "0x73968b9a57c6E53d41345FD57a6E6ae27d6CDB2F", + "TIMELOCK": "0xD3cFc4E65a73BB6C482383EB38f5C3E1d1411616", + "MASTERCHEF": "0xfEA5E213bbD81A8a94D0E1eDB09dBD7CEab61e1c", + "GOVERNANCE_STAKING": "0xBe3e89D333ff21F920656f02c2400E7720Cd782B", + "TREASURY_ZAP": "0x1Ee1a4C8D792E1a68B017fF7cd24A3ce264feBFC", + "TREASURY_VAULT": "0x9D75C85f864Ab9149E23F27C35addaE09B9B909C", + "BADGER_HUNT": "0x22Ba8eEac5bDDf7b5A298D2169A24c6f9e12baA0", + "CURVE_YCRV_VOTER": "0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6", + "SBTC_CRV": "0x075b1bb99792c9E1041bA13afEf80C91a1e70fB3", + "THREE_CRV": "0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490", + "STRATEGIST": "0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063", + "CRV": "0xD533a949740bb3306d119CC777fa900bA034cd52", + "STRATEGY_PROXY": "0xd2793326686F37C7b266541aBc0fdD22eB1E46e8", + "CONTROLLER": "0x29D3782825432255041Db2EAfCB7174f5273f08A", + "THREE_POOL_VAULT": "0xB17640796e4c27a39AF51887aff3F8DC0daF9567", + "STRATEGY_CURVE_3CRV_VOTER_PROXY": "0x4e205567aBEAc593901a443c1d6C777b7a82e789", + "SBTC_VAULT": "0x24129B935AfF071c4f0554882C0D9573F4975fEd", + "STRATEGY_CURVE_BTC_VOTER_PROXY": "0xEDf53517b700383fcA4f823DCE82eF14cAAe2Ea5", + "VE_CURVE_VAULT": "0x478bBC744811eE8310B461514BDc29D03739084D", + "OLD_VE_CURVE_VAULT": "0x2B82FB2B4bac16a1188f377D6a913f235715031b", + "VESTING_ESCROW": "0xC78fA2aF0cA7990bB5FF32C9A728125BE58cf247", + "EURS_CRV_VAULT": "0xCD6997334867728ba14d7922f72c893fcee70e84", + "STRATEGY_CURVE_EURS_CRV_VOTER_PROXY": "0xDb4eA3415a1CA121D9D845110B2D1022f676A0da", + "SANCTUARY": "0xaC14864ce5A98aF3248Ffbf549441b04421247D3", + "XTOKEN_WRAPPER": "0x829C3d9B01eb54a6acfffb06183B1FD489DA5E44" + }, + "prod": { + "UNISWAP_EARLY_LIQUIDITY": "0x13BB257fD618534cc9a0aBBaEE2BD76B0CF11344", + "POOL_LIQUIDITY": 100000000000000000000000, + "UNISWAP_POOL": "0xc465C0a16228Ef6fE1bF29C04Fdb04bb797fd537", + "SUSHISWAP_POOL": "0x22DEF8cF4E481417cb014D9dc64975BA12E3a184", + "AIRDROP_MERKLE_ROOT": "0x43ce90b1f16a7466087f2d22fe1cea4ff420052019b24d0a7b96a9d235caf910", + "ZKSYNC_AIRDROP_MERKLE_ROOT": "0xee170cda08fbcc441aed93461d15faefdb4fbda22462bd031d2ac35dfc6c4fd9", + "SDT_PER_BLOCK": 5000000000000000000, + "AIR_DROP_AMOUNT": 1500000000000000000000000, + "DEAD_EOA": "0x000000000000000000000000000000000000dead", + "CONTRIBUTORS_AMOUNT": 40000000000000000000000000, + "TOTAL_SDT_CIRCULATION": 100000000000000000000000000, + "EPOCH_DURATION": 86400, + "REWARD_REDUCTION_PER_EPOCH": 1000, + "CLAIM_START_TIME": 32400, + "GRACE_PERIOD": 172800, + "TIMELOCK_DELAY": 28800, + "VESTING_START_TIME": 32400, + "VESTING_END_TIME": 63136800, + "VESTING_FUND_ADMINS": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ], + "CURVE_YCRV_VOTER": "0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6", + "SDT": "0x73968b9a57c6E53d41345FD57a6E6ae27d6CDB2F", + "VESTING_ESCROW": "0xC78fA2aF0cA7990bB5FF32C9A728125BE58cf247", + "TIMELOCK": "0xD3cFc4E65a73BB6C482383EB38f5C3E1d1411616", + "MASTERCHEF": "0xfEA5E213bbD81A8a94D0E1eDB09dBD7CEab61e1c", + "MASTERCHEF_POLYGON": "0x7fff9E51465fE3Fc2FfebDe80E50E2EfB22C47A6", + "GOVERNANCE_STAKING": "0xBe3e89D333ff21F920656f02c2400E7720Cd782B", + "TREASURY_ZAP": "0x1Ee1a4C8D792E1a68B017fF7cd24A3ce264feBFC", + "TREASURY_VAULT": "0x9D75C85f864Ab9149E23F27C35addaE09B9B909C", + "BADGER_HUNT": "0x22Ba8eEac5bDDf7b5A298D2169A24c6f9e12baA0", + "MASTERCHEF_BONUS_END_BLOCK": 91215, + "GNOSIS_SAFE_PROXY": "0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063", + "GNOSISSAFE_MASTERCOPY": "0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F", + "GNOSISSAFE_PROXY_FACTORY": "0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B", + "DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "EURS_CRV": "0x194eBd173F6cDacE046C53eACcE9B953F28411d1", + "SBTC_CRV": "0x075b1bb99792c9E1041bA13afEf80C91a1e70fB3", + "THREE_CRV": "0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490", + "CRV": "0xD533a949740bb3306d119CC777fa900bA034cd52", + "STRATEGIST": "0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063", + "ESCROW": "0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063", + "BADGER_HUNT_OWNER": "0xF930EBBd05eF8b25B1797b9b2109DDC9B0d43063", + "VE_CURVE_VAULT": "0x478bBC744811eE8310B461514BDc29D03739084D", + "OLD_VE_CURVE_VAULT": "0x2B82FB2B4bac16a1188f377D6a913f235715031b", + "STRATEGY_PROXY": "0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A", + "CONTROLLER": "0x29D3782825432255041Db2EAfCB7174f5273f08A", + "THREE_POOL_VAULT": "0xB17640796e4c27a39AF51887aff3F8DC0daF9567", + "STRATEGY_CURVE_3CRV_VOTER_PROXY": "0x4e205567aBEAc593901a443c1d6C777b7a82e789", + "EURS_CRV_VAULT": "0xCD6997334867728ba14d7922f72c893fcee70e84", + "STRATEGY_CURVE_EURS_CRV_VOTER_PROXY": "0xDb4eA3415a1CA121D9D845110B2D1022f676A0da", + "SBTC_VAULT": "0x24129B935AfF071c4f0554882C0D9573F4975fEd", + "STRATEGY_CURVE_BTC_VOTER_PROXY": "0xEDf53517b700383fcA4f823DCE82eF14cAAe2Ea5", + "SANCTUARY": "0xaC14864ce5A98aF3248Ffbf549441b04421247D3" + } +} diff --git a/protocol/contracts/README.md b/protocol/contracts/README.md new file mode 100644 index 0000000..6ba88b3 --- /dev/null +++ b/protocol/contracts/README.md @@ -0,0 +1,10 @@ +# protocol/contracts + +All contract sources are within this directory. + +## Contracts + +* [`VestingEscrow`](VestingEscrow.vy): Vests SDT tokens for multiple addresses over multiple vesting periods +* [`VestingEscrowFactory`](VestingEscrowFactory.vy): Factory to store SDT and deploy many simplified vesting contracts + +TODO: Need to add all other contracts diff --git a/protocol/contracts/backscratcher/FeeDistributions.sol b/protocol/contracts/backscratcher/FeeDistributions.sol new file mode 100644 index 0000000..3585a65 --- /dev/null +++ b/protocol/contracts/backscratcher/FeeDistributions.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +contract FeeDistributions { + constructor() public {} + + function claim(address _user) external returns (uint256) { + return 3; + } +} diff --git a/protocol/contracts/backscratcher/StrategyProxys.sol b/protocol/contracts/backscratcher/StrategyProxys.sol new file mode 100644 index 0000000..ff66739 --- /dev/null +++ b/protocol/contracts/backscratcher/StrategyProxys.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +contract StrategyProxys { + address public sdveCRV; + + constructor() public {} + + function claim(address _user) external returns (uint256) { + return 3; + } + + function setSdveCRV(address _sdveCRV) external { + sdveCRV = _sdveCRV; + } +} diff --git a/protocol/contracts/backscratcher/veCurveVault.sol b/protocol/contracts/backscratcher/veCurveVault.sol new file mode 100644 index 0000000..70fd1e1 --- /dev/null +++ b/protocol/contracts/backscratcher/veCurveVault.sol @@ -0,0 +1,563 @@ +/** + *Submitted for verification at Etherscan.io on 2020-11-05 + */ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.12; + +import "../temp/openzeppelin/IERC20.sol"; +import "../temp/openzeppelin/SafeMath.sol"; + +interface StrategyProxy { + function lock() external; +} + +interface FeeDistribution { + function claim(address) external; +} + +contract veCurveVault { + using SafeMath for uint256; + + /// @notice EIP-20 token name for this token + string public constant name = "veCRV Stake DAO"; + + /// @notice EIP-20 token symbol for this token + string public constant symbol = "sdveCRV-DAO"; + + /// @notice EIP-20 token decimals for this token + uint8 public constant decimals = 18; + + /// @notice Total number of tokens in circulation + uint256 public totalSupply = 0; // Initial 0 + + /// @notice A record of each accounts delegate + mapping(address => address) public delegates; + + /// @notice A record of votes checkpoints for each account, by index + mapping(address => mapping(uint32 => Checkpoint)) public checkpoints; + + /// @notice The number of checkpoints for each account + mapping(address => uint32) public numCheckpoints; + + mapping(address => mapping(address => uint256)) internal allowances; + mapping(address => uint256) internal balances; + + /// @notice The EIP-712 typehash for the contract's domain + bytes32 public constant DOMAIN_TYPEHASH = + keccak256( + "EIP712Domain(string name,uint chainId,address verifyingContract)" + ); + bytes32 public immutable DOMAINSEPARATOR; + + /// @notice The EIP-712 typehash for the delegation struct used by the contract + bytes32 public constant DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint nonce,uint expiry)"); + + /// @notice The EIP-712 typehash for the permit struct used by the contract + bytes32 public constant PERMIT_TYPEHASH = + keccak256( + "Permit(address owner,address spender,uint value,uint nonce,uint deadline)" + ); + + /// @notice A record of states for signing / validating signatures + mapping(address => uint256) public nonces; + + /// @notice An event thats emitted when an account changes its delegate + event DelegateChanged( + address indexed delegator, + address indexed fromDelegate, + address indexed toDelegate + ); + + /// @notice An event thats emitted when a delegate account's vote balance changes + event DelegateVotesChanged( + address indexed delegate, + uint256 previousBalance, + uint256 newBalance + ); + + /// @notice A checkpoint for marking number of votes from a given block + struct Checkpoint { + uint32 fromBlock; + uint256 votes; + } + + /** + * @notice Delegate votes from `msg.sender` to `delegatee` + * @param delegatee The address to delegate votes to + */ + function delegate(address delegatee) public { + _delegate(msg.sender, delegatee); + } + + /** + * @notice Delegates votes 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(DELEGATION_TYPEHASH, delegatee, nonce, expiry) + ); + bytes32 digest = + keccak256( + abi.encodePacked("\x19\x01", DOMAINSEPARATOR, structHash) + ); + address signatory = ecrecover(digest, v, r, s); + require(signatory != address(0), "delegateBySig: sig"); + require(nonce == nonces[signatory]++, "delegateBySig: nonce"); + require(now <= expiry, "delegateBySig: expired"); + _delegate(signatory, delegatee); + } + + /** + * @notice Gets the current votes balance for `account` + * @param account The address to get votes balance + * @return The number of current votes for `account` + */ + function getCurrentVotes(address account) external view returns (uint256) { + uint32 nCheckpoints = numCheckpoints[account]; + return + nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0; + } + + /** + * @notice Determine the prior number of votes for an account as of a block number + * @dev Block number must be a finalized block or else this function will revert to prevent misinformation. + * @param account The address of the account to check + * @param blockNumber The block number to get the vote balance at + * @return The number of votes the account had as of the given block + */ + function getPriorVotes(address account, uint256 blockNumber) + public + view + returns (uint256) + { + require(blockNumber < block.number, "getPriorVotes:"); + + uint32 nCheckpoints = numCheckpoints[account]; + if (nCheckpoints == 0) { + return 0; + } + + // First check most recent balance + if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) { + return checkpoints[account][nCheckpoints - 1].votes; + } + + // Next check implicit zero balance + if (checkpoints[account][0].fromBlock > blockNumber) { + return 0; + } + + uint32 lower = 0; + uint32 upper = nCheckpoints - 1; + while (upper > lower) { + uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow + Checkpoint memory cp = checkpoints[account][center]; + if (cp.fromBlock == blockNumber) { + return cp.votes; + } else if (cp.fromBlock < blockNumber) { + lower = center; + } else { + upper = center - 1; + } + } + return checkpoints[account][lower].votes; + } + + function _delegate(address delegator, address delegatee) internal { + address currentDelegate = delegates[delegator]; + uint256 delegatorBalance = balances[delegator]; + delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveDelegates(currentDelegate, delegatee, delegatorBalance); + } + + function _moveDelegates( + address srcRep, + address dstRep, + uint256 amount + ) internal { + if (srcRep != dstRep && amount > 0) { + if (srcRep != address(0)) { + uint32 srcRepNum = numCheckpoints[srcRep]; + uint256 srcRepOld = + srcRepNum > 0 + ? checkpoints[srcRep][srcRepNum - 1].votes + : 0; + uint256 srcRepNew = + srcRepOld.sub(amount, "_moveVotes: underflows"); + _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew); + } + + if (dstRep != address(0)) { + uint32 dstRepNum = numCheckpoints[dstRep]; + uint256 dstRepOld = + dstRepNum > 0 + ? checkpoints[dstRep][dstRepNum - 1].votes + : 0; + uint256 dstRepNew = dstRepOld.add(amount); + _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew); + } + } + } + + function _writeCheckpoint( + address delegatee, + uint32 nCheckpoints, + uint256 oldVotes, + uint256 newVotes + ) internal { + uint32 blockNumber = safe32(block.number, "_writeCheckpoint: 32 bits"); + + if ( + nCheckpoints > 0 && + checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber + ) { + checkpoints[delegatee][nCheckpoints - 1].votes = newVotes; + } else { + checkpoints[delegatee][nCheckpoints] = Checkpoint( + blockNumber, + newVotes + ); + numCheckpoints[delegatee] = nCheckpoints + 1; + } + + emit DelegateVotesChanged(delegatee, oldVotes, newVotes); + } + + function safe32(uint256 n, string memory errorMessage) + internal + pure + returns (uint32) + { + require(n < 2**32, errorMessage); + return uint32(n); + } + + /// @notice The standard EIP-20 transfer event + event Transfer(address indexed from, address indexed to, uint256 amount); + + /// @notice The standard EIP-20 approval event + event Approval( + address indexed owner, + address indexed spender, + uint256 amount + ); + + /// @notice governance address for the governance contract + address public governance; + address public pendingGovernance; + + IERC20 public constant CRV = + IERC20(0xD533a949740bb3306d119CC777fa900bA034cd52); + /* address public constant LOCK = address(0xF147b8125d2ef93FB6965Db97D6746952a133934); */ + address public constant LOCK = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + address public proxy = address(0x7A1848e7847F3f5FfB4d8e63BdB9569db535A4f0); + address public feeDistribution; + + IERC20 public constant rewards = + IERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + + uint256 public index = 0; + uint256 public bal = 0; + + mapping(address => uint256) public supplyIndex; + + function update() external { + _update(); + } + + function _update() internal { + if (totalSupply > 0) { + _claim(); + uint256 _bal = rewards.balanceOf(address(this)); + if (_bal > bal) { + uint256 _diff = _bal.sub(bal, "veCRV::_update: bal _diff"); + if (_diff > 0) { + uint256 _ratio = _diff.mul(1e18).div(totalSupply); + if (_ratio > 0) { + index = index.add(_ratio); + bal = _bal; + } + } + } + } + } + + function _claim() internal { + if (feeDistribution != address(0x0)) { + FeeDistribution(feeDistribution).claim(address(this)); + } + } + + function updateFor(address recipient) public { + _update(); + uint256 _supplied = balances[recipient]; + if (_supplied > 0) { + uint256 _supplyIndex = supplyIndex[recipient]; + supplyIndex[recipient] = index; + uint256 _delta = + index.sub(_supplyIndex, "veCRV::_claimFor: index delta"); + if (_delta > 0) { + uint256 _share = _supplied.mul(_delta).div(1e18); + claimable[recipient] = claimable[recipient].add(_share); + } + } else { + supplyIndex[recipient] = index; + } + } + + mapping(address => uint256) public claimable; + + function claim() external { + _claimFor(msg.sender); + } + + function claimFor(address recipient) external { + _claimFor(recipient); + } + + function _claimFor(address recipient) internal { + updateFor(recipient); + rewards.transfer(recipient, claimable[recipient]); + claimable[recipient] = 0; + bal = rewards.balanceOf(address(this)); + } + + constructor() public { + // Set governance for this token + governance = msg.sender; + DOMAINSEPARATOR = keccak256( + abi.encode( + DOMAIN_TYPEHASH, + keccak256(bytes(name)), + _getChainId(), + address(this) + ) + ); + } + + function _mint(address dst, uint256 amount) internal { + updateFor(dst); + // mint the amount + totalSupply = totalSupply.add(amount); + // transfer the amount to the recipient + balances[dst] = balances[dst].add(amount); + emit Transfer(address(0), dst, amount); + + // move delegates + _moveDelegates(address(0), delegates[dst], amount); + } + + function depositAll() external { + _deposit(CRV.balanceOf(msg.sender)); + } + + function deposit(uint256 _amount) external { + _deposit(_amount); + } + + function _deposit(uint256 _amount) internal { + CRV.transferFrom(msg.sender, LOCK, _amount); + _mint(msg.sender, _amount); + StrategyProxy(proxy).lock(); + } + + function setProxy(address _proxy) external { + require(msg.sender == governance, "setGovernance: !gov"); + proxy = _proxy; + } + + function setFeeDistribution(address _feeDistribution) external { + require(msg.sender == governance, "setGovernance: !gov"); + feeDistribution = _feeDistribution; + } + + /** + * @notice Allows governance to change governance (for future upgradability) + * @param _governance new governance address to set + */ + function setGovernance(address _governance) external { + require(msg.sender == governance, "setGovernance: !gov"); + pendingGovernance = _governance; + } + + /** + * @notice Allows pendingGovernance to accept their role as governance (protection pattern) + */ + function acceptGovernance() external { + require( + msg.sender == pendingGovernance, + "acceptGovernance: !pendingGov" + ); + governance = pendingGovernance; + } + + /** + * @notice Get the number of tokens `spender` is approved to spend on behalf of `account` + * @param account The address of the account holding the funds + * @param spender The address of the account spending the funds + * @return The number of tokens approved + */ + function allowance(address account, address spender) + external + view + returns (uint256) + { + return allowances[account][spender]; + } + + /** + * @notice Approve `spender` to transfer up to `amount` from `src` + * @dev This will overwrite the approval amount for `spender` + * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) + * @param spender The address of the account which may transfer tokens + * @param amount The number of tokens that are approved (2^256-1 means infinite) + * @return Whether or not the approval succeeded + */ + function approve(address spender, uint256 amount) external returns (bool) { + allowances[msg.sender][spender] = amount; + + emit Approval(msg.sender, spender, amount); + return true; + } + + /** + * @notice Triggers an approval from owner to spends + * @param owner The address to approve from + * @param spender The address to be approved + * @param amount The number of tokens that are approved (2^256-1 means infinite) + * @param deadline 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 permit( + address owner, + address spender, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + bytes32 structHash = + keccak256( + abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + amount, + nonces[owner]++, + deadline + ) + ); + bytes32 digest = + keccak256( + abi.encodePacked("\x19\x01", DOMAINSEPARATOR, structHash) + ); + address signatory = ecrecover(digest, v, r, s); + require(signatory != address(0), "permit: signature"); + require(signatory == owner, "permit: unauthorized"); + require(now <= deadline, "permit: expired"); + + allowances[owner][spender] = amount; + + emit Approval(owner, spender, amount); + } + + /** + * @notice Get the number of tokens held by the `account` + * @param account The address of the account to get the balance of + * @return The number of tokens held + */ + function balanceOf(address account) external view returns (uint256) { + return balances[account]; + } + + /** + * @notice Transfer `amount` tokens from `msg.sender` to `dst` + * @param dst The address of the destination account + * @param amount The number of tokens to transfer + * @return Whether or not the transfer succeeded + */ + function transfer(address dst, uint256 amount) external returns (bool) { + _transferTokens(msg.sender, dst, amount); + return true; + } + + /** + * @notice Transfer `amount` tokens from `src` to `dst` + * @param src The address of the source account + * @param dst The address of the destination account + * @param amount The number of tokens to transfer + * @return Whether or not the transfer succeeded + */ + function transferFrom( + address src, + address dst, + uint256 amount + ) external returns (bool) { + address spender = msg.sender; + uint256 spenderAllowance = allowances[src][spender]; + + if (spender != src && spenderAllowance != uint256(-1)) { + uint256 newAllowance = + spenderAllowance.sub( + amount, + "transferFrom: exceeds spender allowance" + ); + allowances[src][spender] = newAllowance; + + emit Approval(src, spender, newAllowance); + } + + _transferTokens(src, dst, amount); + return true; + } + + function _transferTokens( + address src, + address dst, + uint256 amount + ) internal { + require(src != address(0), "_transferTokens: zero address"); + require(dst != address(0), "_transferTokens: zero address"); + + updateFor(src); + updateFor(dst); + + balances[src] = balances[src].sub( + amount, + "_transferTokens: exceeds balance" + ); + balances[dst] = balances[dst].add(amount, "_transferTokens: overflows"); + emit Transfer(src, dst, amount); + } + + function _getChainId() internal pure returns (uint256) { + uint256 chainId; + assembly { + chainId := chainid() + } + return chainId; + } +} diff --git a/protocol/contracts/backscratcher/zap/CurveBackzapper.vy b/protocol/contracts/backscratcher/zap/CurveBackzapper.vy new file mode 100644 index 0000000..46d4a39 --- /dev/null +++ b/protocol/contracts/backscratcher/zap/CurveBackzapper.vy @@ -0,0 +1,42 @@ +# @version 0.2.7 + +from vyper.interfaces import ERC20 + +interface Minter: + def mint_for(gauge_addr: address, _for: address): nonpayable + def minted(addr: address, gauge: address) -> uint256: view + +interface VestingEscrow: + def balanceOf(addr: address) -> uint256: view + def claim(addr: address): nonpayable + +interface Vault: + def deposit(amount: uint256): nonpayable + def transfer(addr: address, amount: uint256) -> bool: nonpayable + def balanceOf(addr: address) -> uint256: view + +crv: public(ERC20) +vault: public(Vault) +vesting: public(VestingEscrow) +minter: public(Minter) + +@external +def __init__(): + self.crv = ERC20(0xD533a949740bb3306d119CC777fa900bA034cd52) + self.vault = Vault(0xc5bDdf9843308380375a611c18B50Fb9341f502A) + self.vesting = VestingEscrow(0x575CCD8e2D300e2377B43478339E364000318E2c) + self.minter = Minter(0xd061D61a4d941c39E5453435B6345Dc261C2fcE0) + self.crv.approve(self.vault.address, MAX_UINT256) + +@external +def zap(gauges: address[20]): + # Enable with minter.toggle_approve_mint(self) + for i in range(20): + if gauges[i] == ZERO_ADDRESS: + break + self.minter.mint_for(gauges[i], msg.sender) + + self.vesting.claim(msg.sender) + self.crv.transferFrom(msg.sender, self, self.crv.balanceOf(msg.sender)) + self.vault.deposit(self.crv.balanceOf(self)) + self.vault.transfer(msg.sender, self.vault.balanceOf(self)) \ No newline at end of file diff --git a/protocol/contracts/backscratcher/zap/y3CrvZapper.vy b/protocol/contracts/backscratcher/zap/y3CrvZapper.vy new file mode 100644 index 0000000..83bbb49 --- /dev/null +++ b/protocol/contracts/backscratcher/zap/y3CrvZapper.vy @@ -0,0 +1,30 @@ +# @version 0.2.7 + +from vyper.interfaces import ERC20 + +interface Vault: + def deposit(amount: uint256): nonpayable + def transfer(addr: address, amount: uint256) -> bool: nonpayable + def balanceOf(addr: address) -> uint256: view + +interface Claimable: + def claimFor(recipient: address): nonpayable + def claim(recipient: address): nonpayable + +threeCrv: public(ERC20) +threeCrvVault: public(Vault) +vecrvVault: public(Claimable) + +@external +def __init__(): + self.threeCrv = ERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490) + self.vecrvVault = Claimable(0xc5bDdf9843308380375a611c18B50Fb9341f502A) + self.threeCrvVault = Vault(0x9cA85572E6A3EbF24dEDd195623F188735A5179f) + self.threeCrv.approve(self.threeCrvVault.address, MAX_UINT256) + +@external +def zap(): + self.vecrvVault.claimFor(msg.sender) + self.threeCrv.transferFrom(msg.sender, self, self.threeCrv.balanceOf(msg.sender)) + self.threeCrvVault.deposit(self.threeCrv.balanceOf(self)) + self.threeCrvVault.transfer(msg.sender, self.threeCrvVault.balanceOf(self)) diff --git a/protocol/contracts/badger-hunt/BadgerHunt.sol b/protocol/contracts/badger-hunt/BadgerHunt.sol new file mode 100644 index 0000000..914fdcc --- /dev/null +++ b/protocol/contracts/badger-hunt/BadgerHunt.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.11; + +import "../temp/openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "../temp/openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "../temp/openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "../temp/openzeppelin/contracts-upgradeable/cryptography/MerkleProofUpgradeable.sol"; +import "../../interfaces/badger/IMerkleDistributor.sol"; +import "./MerkleDistributor.sol"; + +contract BadgerHunt is MerkleDistributor, OwnableUpgradeable { + using SafeMathUpgradeable for uint256; + uint256 public constant MAX_BPS = 10000; + + uint256 public claimsStart; + uint256 public gracePeriod; + + uint256 public epochDuration; + uint256 public rewardReductionPerEpoch; + uint256 public currentRewardRate; + uint256 public finalEpoch; + + address public rewardsEscrow; + + event Hunt( + uint256 index, + address indexed account, + uint256 amount, + uint256 userClaim, + uint256 rewardsEscrowClaim + ); + + function initialize( + address token_, + bytes32 merkleRoot_, + uint256 epochDuration_, + uint256 rewardReductionPerEpoch_, + uint256 claimsStart_, + uint256 gracePeriod_, + address rewardsEscrow_, + address owner_ + ) public initializer { + __MerkleDistributor_init(token_, merkleRoot_); + + __Ownable_init(); + transferOwnership(owner_); + + epochDuration = epochDuration_; + rewardReductionPerEpoch = rewardReductionPerEpoch_; + claimsStart = claimsStart_; + gracePeriod = gracePeriod_; + + rewardsEscrow = rewardsEscrow_; + + currentRewardRate = 10000; + + finalEpoch = (currentRewardRate / rewardReductionPerEpoch_) - 1; + } + + /// ===== View Functions ===== + /// @dev Get grace period end timestamp + function getGracePeriodEnd() public view returns (uint256) { + return claimsStart.add(gracePeriod); + } + + /// @dev Get claims start timestamp + function getClaimsStartTime() public view returns (uint256) { + return claimsStart; + } + + /// @dev Get the next epoch start + function getNextEpochStart() public view returns (uint256) { + uint256 epoch = getCurrentEpoch(); + + if (epoch == 0) { + return getGracePeriodEnd(); + } else { + return getGracePeriodEnd().add(epochDuration.mul(epoch)); + } + } + + function getTimeUntilNextEpoch() public view returns (uint256) { + uint256 epoch = getCurrentEpoch(); + + if (epoch == 0) { + return getGracePeriodEnd().sub(now); + } else { + return (getGracePeriodEnd().add(epochDuration.mul(epoch))).sub(now); + } + } + + /// @dev Get the current epoch number + function getCurrentEpoch() public view returns (uint256) { + uint256 gracePeriodEnd = claimsStart.add(gracePeriod); + + if (now < gracePeriodEnd) { + return 0; + } + uint256 secondsPastGracePeriod = now.sub(gracePeriodEnd); + return (secondsPastGracePeriod / epochDuration).add(1); + } + + /// @dev Get the rewards % of current epoch + function getCurrentRewardsRate() public view returns (uint256) { + uint256 epoch = getCurrentEpoch(); + if (epoch == 0) return MAX_BPS; + if (epoch > finalEpoch) return 0; + else return MAX_BPS.sub(epoch.mul(rewardReductionPerEpoch)); + } + + /// @dev Get the rewards % of following epoch + function getNextEpochRewardsRate() public view returns (uint256) { + uint256 epoch = getCurrentEpoch().add(1); + if (epoch == 0) return MAX_BPS; + if (epoch > finalEpoch) return 0; + else return MAX_BPS.sub(epoch.mul(rewardReductionPerEpoch)); + } + + /// ===== Public Actions ===== + + function claim( + uint256 index, + address account, + uint256 amount, + bytes32[] calldata merkleProof + ) external virtual override { + require(now >= claimsStart, "BadgerDistributor: Before claim start."); + require( + account == msg.sender, + "BadgerDistributor: Can only claim for own account." + ); + require( + getCurrentRewardsRate() > 0, + "BadgerDistributor: Past rewards claim period." + ); + require(!isClaimed(index), "BadgerDistributor: Drop already claimed."); + + // Verify the merkle proof. + bytes32 node = keccak256(abi.encodePacked(index, account, amount)); + require( + MerkleProofUpgradeable.verify(merkleProof, merkleRoot, node), + "BadgerDistributor: Invalid proof." + ); + + // Mark it claimed and send the token. + _setClaimed(index); + + require(getCurrentRewardsRate() <= MAX_BPS, "Excessive Rewards Rate"); + uint256 claimable = amount.mul(getCurrentRewardsRate()).div(MAX_BPS); + + require( + IERC20Upgradeable(token).transfer(account, claimable), + "Transfer to user failed." + ); + emit Hunt(index, account, amount, claimable, amount.sub(claimable)); + } + + /// ===== Gated Actions: Owner ===== + + /// @notice After hunt is complete, transfer excess funds to rewardsEscrow + function recycleExcess() external onlyOwner { + require( + getCurrentRewardsRate() == 0 && getCurrentEpoch() > finalEpoch, + "Hunt period not finished" + ); + uint256 remainingBalance = IERC20Upgradeable(token).balanceOf( + address(this) + ); + IERC20Upgradeable(token).transfer(rewardsEscrow, remainingBalance); + } + + function setGracePeriod(uint256 duration) external onlyOwner { + gracePeriod = duration; + } +} diff --git a/protocol/contracts/badger-hunt/MerkleDistributor.sol b/protocol/contracts/badger-hunt/MerkleDistributor.sol new file mode 100644 index 0000000..c45b12f --- /dev/null +++ b/protocol/contracts/badger-hunt/MerkleDistributor.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.11; + +import "../temp/openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "../temp/openzeppelin/contracts-upgradeable/cryptography/MerkleProofUpgradeable.sol"; +import "../temp/openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; +import "../../interfaces/badger/IMerkleDistributor.sol"; + +contract MerkleDistributor is Initializable, IMerkleDistributor { + address public token; + bytes32 public merkleRoot; + + // This is a packed array of booleans. + mapping(uint256 => uint256) internal claimedBitMap; + + function __MerkleDistributor_init(address token_, bytes32 merkleRoot_) + public + initializer + { + token = token_; + merkleRoot = merkleRoot_; + } + + function isClaimed(uint256 index) public override view returns (bool) { + uint256 claimedWordIndex = index / 256; + uint256 claimedBitIndex = index % 256; + uint256 claimedWord = claimedBitMap[claimedWordIndex]; + uint256 mask = (1 << claimedBitIndex); + return claimedWord & mask == mask; + } + + function _setClaimed(uint256 index) internal { + uint256 claimedWordIndex = index / 256; + uint256 claimedBitIndex = index % 256; + claimedBitMap[claimedWordIndex] = + claimedBitMap[claimedWordIndex] | + (1 << claimedBitIndex); + } + + function claim( + uint256 index, + address account, + uint256 amount, + bytes32[] calldata merkleProof + ) external virtual override { + require(!isClaimed(index), "MerkleDistributor: Drop already claimed."); + + // Verify the merkle proof. + bytes32 node = keccak256(abi.encodePacked(index, account, amount)); + require( + MerkleProofUpgradeable.verify(merkleProof, merkleRoot, node), + "MerkleDistributor: Invalid proof." + ); + + // Mark it claimed and send the token. + _setClaimed(index); + require( + IERC20Upgradeable(token).transfer(account, amount), + "MerkleDistributor: Transfer failed." + ); + + emit Claimed(index, account, amount); + } +} diff --git a/protocol/contracts/badger-hunt/README.md b/protocol/contracts/badger-hunt/README.md new file mode 100644 index 0000000..cd6a670 --- /dev/null +++ b/protocol/contracts/badger-hunt/README.md @@ -0,0 +1,19 @@ +# Badger Hunt + +A gamified, retroactive airdrop based on past Bitcoin + DeFi activities. + +- A wide variety of accounts will recieve airdrops based on past DeFi actions. +- For 24 hours, each account is able to claim it's full amount. +- At the end of that 24 hours and after each subsequent day, 20% less of the reward will be claimable if not claimed + - This 'lost' reward will go to a pool that can be used for future airdrops, or eventually returned to the Badger DAO + +## Variant: Honeypots + +Pools of Badger that can be unlocked with by meeting a series of conditions + +### Meme NFTS + +To claim the pool, the user must: + +- Have ownership of a series of NFTs +- Input a secret, which can be derived from those NFTs diff --git a/protocol/contracts/controllers/Controller.sol b/protocol/contracts/controllers/Controller.sol new file mode 100644 index 0000000..16db9cd --- /dev/null +++ b/protocol/contracts/controllers/Controller.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/yearn/IConverter.sol"; +import "../../interfaces/yearn/IOneSplitAudit.sol"; +import "../../interfaces/yearn/IStrategy.sol"; + +contract Controller { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public governance; + address public strategist; + + address public onesplit; + address public rewards; + mapping(address => address) public vaults; + mapping(address => address) public strategies; + mapping(address => mapping(address => address)) public converters; + + mapping(address => mapping(address => bool)) public approvedStrategies; + + uint256 public split = 500; + uint256 public constant max = 10000; + + constructor(address _rewards) public { + governance = msg.sender; + strategist = msg.sender; + onesplit = address(0x50FDA034C0Ce7a8f7EFDAebDA7Aa7cA21CC1267e); + rewards = _rewards; + } + + function setRewards(address _rewards) public { + require(msg.sender == governance, "!governance"); + rewards = _rewards; + } + + function setStrategist(address _strategist) public { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function setSplit(uint256 _split) public { + require(msg.sender == governance, "!governance"); + split = _split; + } + + function setOneSplit(address _onesplit) public { + require(msg.sender == governance, "!governance"); + onesplit = _onesplit; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setVault(address _token, address _vault) public { + require(msg.sender == strategist || msg.sender == governance, "!strategist"); + require(vaults[_token] == address(0), "vault"); + vaults[_token] = _vault; + } + + function approveStrategy(address _token, address _strategy) public { + require(msg.sender == governance, "!governance"); + approvedStrategies[_token][_strategy] = true; + } + + function revokeStrategy(address _token, address _strategy) public { + require(msg.sender == governance, "!governance"); + approvedStrategies[_token][_strategy] = false; + } + + function setConverter( + address _input, + address _output, + address _converter + ) public { + require(msg.sender == strategist || msg.sender == governance, "!strategist"); + converters[_input][_output] = _converter; + } + + function setStrategy(address _token, address _strategy) public { + require(msg.sender == strategist || msg.sender == governance, "!strategist"); + require(approvedStrategies[_token][_strategy] == true, "!approved"); + + address _current = strategies[_token]; + if (_current != address(0)) { + IStrategy(_current).withdrawAll(); + } + strategies[_token] = _strategy; + } + + function earn(address _token, uint256 _amount) public { + address _strategy = strategies[_token]; + address _want = IStrategy(_strategy).want(); + if (_want != _token) { + address converter = converters[_token][_want]; + IERC20(_token).safeTransfer(converter, _amount); + _amount = IConverter(converter).convert(_strategy); + IERC20(_want).safeTransfer(_strategy, _amount); + } else { + IERC20(_token).safeTransfer(_strategy, _amount); + } + IStrategy(_strategy).deposit(); + } + + function balanceOf(address _token) external view returns (uint256) { + return IStrategy(strategies[_token]).balanceOf(); + } + + function withdrawAll(address _token) public { + require(msg.sender == strategist || msg.sender == governance, "!strategist"); + IStrategy(strategies[_token]).withdrawAll(); + } + + function inCaseTokensGetStuck(address _token, uint256 _amount) public { + require(msg.sender == strategist || msg.sender == governance, "!governance"); + IERC20(_token).safeTransfer(msg.sender, _amount); + } + + function inCaseStrategyTokenGetStuck(address _strategy, address _token) public { + require(msg.sender == strategist || msg.sender == governance, "!governance"); + IStrategy(_strategy).withdraw(_token); + } + + function getExpectedReturn( + address _strategy, + address _token, + uint256 parts + ) public view returns (uint256 expected) { + uint256 _balance = IERC20(_token).balanceOf(_strategy); + address _want = IStrategy(_strategy).want(); + (expected, ) = IOneSplitAudit(onesplit).getExpectedReturn(_token, _want, _balance, parts, 0); + } + + // Only allows to withdraw non-core strategy tokens ~ this is over and above normal yield + function yearn( + address _strategy, + address _token, + uint256 parts + ) public { + require(msg.sender == strategist || msg.sender == governance, "!governance"); + // This contract should never have value in it, but just incase since this is a public call + uint256 _before = IERC20(_token).balanceOf(address(this)); + IStrategy(_strategy).withdraw(_token); + uint256 _after = IERC20(_token).balanceOf(address(this)); + if (_after > _before) { + uint256 _amount = _after.sub(_before); + address _want = IStrategy(_strategy).want(); + uint256[] memory _distribution; + uint256 _expected; + _before = IERC20(_want).balanceOf(address(this)); + IERC20(_token).safeApprove(onesplit, 0); + IERC20(_token).safeApprove(onesplit, _amount); + (_expected, _distribution) = IOneSplitAudit(onesplit).getExpectedReturn(_token, _want, _amount, parts, 0); + IOneSplitAudit(onesplit).swap(_token, _want, _amount, _expected, _distribution, 0); + _after = IERC20(_want).balanceOf(address(this)); + if (_after > _before) { + _amount = _after.sub(_before); + uint256 _reward = _amount.mul(split).div(max); + earn(_want, _amount.sub(_reward)); + IERC20(_want).safeTransfer(rewards, _reward); + } + } + } + + function withdraw(address _token, uint256 _amount) public { + require(msg.sender == vaults[_token], "!vault"); + IStrategy(strategies[_token]).withdraw(_amount); + } +} diff --git a/protocol/contracts/controllers/DelegatedController.sol b/protocol/contracts/controllers/DelegatedController.sol new file mode 100644 index 0000000..ac2f517 --- /dev/null +++ b/protocol/contracts/controllers/DelegatedController.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/yearn/IStrategy.sol"; +import "../../interfaces/yearn/IConverter.sol"; +import "../../interfaces/yearn/IOneSplitAudit.sol"; +import "../../interfaces/yearn/IDelegatedVault.sol"; + +contract DelegatedController { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public governance; + address public onesplit; + address public rewards; + + // Vault to strategy mapping + mapping(address => address) public vaults; + // Strategy to vault mapping + mapping(address => address) public strategies; + + mapping(address => mapping(address => address)) public converters; + + mapping(address => bool) public isVault; + mapping(address => bool) public isStrategy; + + uint256 public split = 500; + uint256 public constant max = 10000; + + constructor(address _rewards) public { + governance = msg.sender; + onesplit = address(0x50FDA034C0Ce7a8f7EFDAebDA7Aa7cA21CC1267e); + rewards = _rewards; + } + + function setSplit(uint256 _split) external { + require(msg.sender == governance, "!governance"); + split = _split; + } + + function setOneSplit(address _onesplit) external { + require(msg.sender == governance, "!governance"); + onesplit = _onesplit; + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setConverter( + address _input, + address _output, + address _converter + ) external { + require(msg.sender == governance, "!governance"); + converters[_input][_output] = _converter; + } + + function setStrategy(address _vault, address _strategy) external { + require(msg.sender == governance, "!governance"); + address _current = strategies[_vault]; + if (_current != address(0)) { + IStrategy(_current).withdrawAll(); + } + strategies[_vault] = _strategy; + isStrategy[_strategy] = true; + vaults[_strategy] = _vault; + isVault[_vault] = true; + } + + function want(address _vault) external view returns (address) { + return IStrategy(strategies[_vault]).want(); + } + + function earn(address _vault, uint256 _amount) public { + address _strategy = strategies[_vault]; + address _want = IStrategy(_strategy).want(); + IERC20(_want).safeTransfer(_strategy, _amount); + IStrategy(_strategy).deposit(); + } + + function balanceOf(address _vault) external view returns (uint256) { + return IStrategy(strategies[_vault]).balanceOf(); + } + + function withdrawAll(address _strategy) external { + require(msg.sender == governance, "!governance"); + // WithdrawAll sends 'want' to 'vault' + IStrategy(_strategy).withdrawAll(); + } + + function inCaseTokensGetStuck(address _token, uint256 _amount) external { + require(msg.sender == governance, "!governance"); + IERC20(_token).safeTransfer(governance, _amount); + } + + function inCaseStrategyGetStruck(address _strategy, address _token) external { + require(msg.sender == governance, "!governance"); + IStrategy(_strategy).withdraw(_token); + IERC20(_token).safeTransfer(governance, IERC20(_token).balanceOf(address(this))); + } + + function getExpectedReturn( + address _strategy, + address _token, + uint256 parts + ) external view returns (uint256 expected) { + uint256 _balance = IERC20(_token).balanceOf(_strategy); + address _want = IStrategy(_strategy).want(); + (expected, ) = IOneSplitAudit(onesplit).getExpectedReturn(_token, _want, _balance, parts, 0); + } + + function claimInsurance(address _vault) external { + require(msg.sender == governance, "!governance"); + IDelegatedVault(_vault).claimInsurance(); + } + + // Only allows to withdraw non-core strategy tokens ~ this is over and above normal yield + function delegatedHarvest(address _strategy, uint256 parts) external { + // This contract should never have value in it, but just incase since this is a public call + address _have = IStrategy(_strategy).want(); + uint256 _before = IERC20(_have).balanceOf(address(this)); + IStrategy(_strategy).skim(); + uint256 _after = IERC20(_have).balanceOf(address(this)); + if (_after > _before) { + uint256 _amount = _after.sub(_before); + address _want = IDelegatedVault(vaults[_strategy]).token(); + uint256[] memory _distribution; + uint256 _expected; + _before = IERC20(_want).balanceOf(address(this)); + IERC20(_have).safeApprove(onesplit, 0); + IERC20(_have).safeApprove(onesplit, _amount); + (_expected, _distribution) = IOneSplitAudit(onesplit).getExpectedReturn(_have, _want, _amount, parts, 0); + IOneSplitAudit(onesplit).swap(_have, _want, _amount, _expected, _distribution, 0); + _after = IERC20(_want).balanceOf(address(this)); + if (_after > _before) { + _amount = _after.sub(_before); + uint256 _reward = _amount.mul(split).div(max); + IERC20(_want).safeTransfer(vaults[_strategy], _amount.sub(_reward)); + IERC20(_want).safeTransfer(rewards, _reward); + } + } + } + + // Only allows to withdraw non-core strategy tokens ~ this is over and above normal yield + function harvest( + address _strategy, + address _token, + uint256 parts + ) external { + // This contract should never have value in it, but just incase since this is a public call + uint256 _before = IERC20(_token).balanceOf(address(this)); + IStrategy(_strategy).withdraw(_token); + uint256 _after = IERC20(_token).balanceOf(address(this)); + if (_after > _before) { + uint256 _amount = _after.sub(_before); + address _want = IStrategy(_strategy).want(); + uint256[] memory _distribution; + uint256 _expected; + _before = IERC20(_want).balanceOf(address(this)); + IERC20(_token).safeApprove(onesplit, 0); + IERC20(_token).safeApprove(onesplit, _amount); + (_expected, _distribution) = IOneSplitAudit(onesplit).getExpectedReturn(_token, _want, _amount, parts, 0); + IOneSplitAudit(onesplit).swap(_token, _want, _amount, _expected, _distribution, 0); + _after = IERC20(_want).balanceOf(address(this)); + if (_after > _before) { + _amount = _after.sub(_before); + uint256 _reward = _amount.mul(split).div(max); + earn(_want, _amount.sub(_reward)); + IERC20(_want).safeTransfer(rewards, _reward); + } + } + } + + function withdraw(address _vault, uint256 _amount) external { + require(isVault[msg.sender] == true, "!vault"); + IStrategy(strategies[_vault]).withdraw(_amount); + } +} diff --git a/protocol/contracts/escrow/VestingEscrow.vy b/protocol/contracts/escrow/VestingEscrow.vy new file mode 100644 index 0000000..7c4c43a --- /dev/null +++ b/protocol/contracts/escrow/VestingEscrow.vy @@ -0,0 +1,275 @@ +# @version 0.2.4 +""" +@title Vesting Escrow +@author Stake DAO +@license MIT +@notice Vests `ERC20SDT` tokens for multiple addresses over multiple vesting periods +""" + + +from vyper.interfaces import ERC20 + +event Fund: + recipient: indexed(address) + amount: uint256 + +event Claim: + recipient: indexed(address) + claimed: uint256 + +event ToggleDisable: + recipient: address + disabled: bool + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + + +token: public(address) +start_time: public(uint256) +end_time: public(uint256) +initial_locked: public(HashMap[address, uint256]) +total_claimed: public(HashMap[address, uint256]) + +initial_locked_supply: public(uint256) +unallocated_supply: public(uint256) + +can_disable: public(bool) +disabled_at: public(HashMap[address, uint256]) + +admin: public(address) +future_admin: public(address) + +fund_admins_enabled: public(bool) +fund_admins: public(HashMap[address, bool]) + + +@external +def __init__( + _token: address, + _start_time: uint256, + _end_time: uint256, + _can_disable: bool, + _fund_admins: address[4] +): + """ + @param _token Address of the ERC20 token being distributed + @param _start_time Timestamp at which the distribution starts. Should be in + the future, so that we have enough time to VoteLock everyone + @param _end_time Time until everything should be vested + @param _can_disable Whether admin can disable accounts in this deployment + @param _fund_admins Temporary admin accounts used only for funding + """ + assert _start_time >= block.timestamp + assert _end_time > _start_time + + self.token = _token + self.admin = msg.sender + self.start_time = _start_time + self.end_time = _end_time + self.can_disable = _can_disable + + _fund_admins_enabled: bool = False + for addr in _fund_admins: + if addr != ZERO_ADDRESS: + self.fund_admins[addr] = True + if not _fund_admins_enabled: + _fund_admins_enabled = True + self.fund_admins_enabled = True + + + +@external +def add_tokens(_amount: uint256): + """ + @notice Transfer vestable tokens into the contract + @dev Handled separate from `fund` to reduce transaction count when using funding admins + @param _amount Number of tokens to transfer + """ + assert msg.sender == self.admin # dev: admin only + assert ERC20(self.token).transferFrom(msg.sender, self, _amount) # dev: transfer failed + self.unallocated_supply += _amount + + +@external +@nonreentrant('lock') +def fund(_recipients: address[100], _amounts: uint256[100]): + """ + @notice Vest tokens for multiple recipients + @param _recipients List of addresses to fund + @param _amounts Amount of vested tokens for each address + """ + if msg.sender != self.admin: + assert self.fund_admins[msg.sender] # dev: admin only + assert self.fund_admins_enabled # dev: fund admins disabled + + _total_amount: uint256 = 0 + for i in range(100): + amount: uint256 = _amounts[i] + recipient: address = _recipients[i] + if recipient == ZERO_ADDRESS: + break + _total_amount += amount + self.initial_locked[recipient] += amount + log Fund(recipient, amount) + + self.initial_locked_supply += _total_amount + self.unallocated_supply -= _total_amount + + +@external +def toggle_disable(_recipient: address): + """ + @notice Disable or re-enable a vested address's ability to claim tokens + @dev When disabled, the address is only unable to claim tokens which are still + locked at the time of this call. It is not possible to block the claim + of tokens which have already vested. + @param _recipient Address to disable or enable + """ + assert msg.sender == self.admin # dev: admin only + assert self.can_disable, "Cannot disable" + + is_disabled: bool = self.disabled_at[_recipient] == 0 + if is_disabled: + self.disabled_at[_recipient] = block.timestamp + else: + self.disabled_at[_recipient] = 0 + + log ToggleDisable(_recipient, is_disabled) + + +@external +def disable_can_disable(): + """ + @notice Disable the ability to call `toggle_disable` + """ + assert msg.sender == self.admin # dev: admin only + self.can_disable = False + + +@external +def disable_fund_admins(): + """ + @notice Disable the funding admin accounts + """ + assert msg.sender == self.admin # dev: admin only + self.fund_admins_enabled = False + + +@internal +@view +def _total_vested_of(_recipient: address, _time: uint256 = block.timestamp) -> uint256: + start: uint256 = self.start_time + end: uint256 = self.end_time + locked: uint256 = self.initial_locked[_recipient] + if _time < start: + return 0 + return min(locked * (_time - start) / (end - start), locked) + + +@internal +@view +def _total_vested() -> uint256: + start: uint256 = self.start_time + end: uint256 = self.end_time + locked: uint256 = self.initial_locked_supply + if block.timestamp < start: + return 0 + return min(locked * (block.timestamp - start) / (end - start), locked) + + +@external +@view +def vestedSupply() -> uint256: + """ + @notice Get the total number of tokens which have vested, that are held + by this contract + """ + return self._total_vested() + + +@external +@view +def lockedSupply() -> uint256: + """ + @notice Get the total number of tokens which are still locked + (have not yet vested) + """ + return self.initial_locked_supply - self._total_vested() + + +@external +@view +def vestedOf(_recipient: address) -> uint256: + """ + @notice Get the number of tokens which have vested for a given address + @param _recipient address to check + """ + return self._total_vested_of(_recipient) + + +@external +@view +def balanceOf(_recipient: address) -> uint256: + """ + @notice Get the number of unclaimed, vested tokens for a given address + @param _recipient address to check + """ + return self._total_vested_of(_recipient) - self.total_claimed[_recipient] + + +@external +@view +def lockedOf(_recipient: address) -> uint256: + """ + @notice Get the number of locked tokens for a given address + @param _recipient address to check + """ + return self.initial_locked[_recipient] - self._total_vested_of(_recipient) + + +@external +@nonreentrant('lock') +def claim(addr: address = msg.sender): + """ + @notice Claim tokens which have vested + @param addr Address to claim tokens for + """ + t: uint256 = self.disabled_at[addr] + if t == 0: + t = block.timestamp + claimable: uint256 = self._total_vested_of(addr, t) - self.total_claimed[addr] + self.total_claimed[addr] += claimable + assert ERC20(self.token).transfer(addr, claimable) + + log Claim(addr, claimable) + + +@external +def commit_transfer_ownership(addr: address) -> bool: + """ + @notice Transfer ownership to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + self.future_admin = addr + log CommitOwnership(addr) + + return True + + +@external +def apply_transfer_ownership() -> bool: + """ + @notice Apply pending ownership transfer + """ + assert msg.sender == self.admin # dev: admin only + _admin: address = self.future_admin + assert _admin != ZERO_ADDRESS # dev: admin not set + self.admin = _admin + log ApplyOwnership(_admin) + + return True diff --git a/protocol/contracts/exploits/EvilGauge.sol b/protocol/contracts/exploits/EvilGauge.sol new file mode 100644 index 0000000..62e0b0c --- /dev/null +++ b/protocol/contracts/exploits/EvilGauge.sol @@ -0,0 +1,17 @@ +pragma solidity =0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; + +contract EvilGauge { + IERC20 token; + address owner; + + constructor(address _token) public { + owner = msg.sender; + token = IERC20(_token); + } + + function deposit(uint256 amount) public { + token.transferFrom(msg.sender, owner, amount); + } +} diff --git a/protocol/contracts/external/Gauge.vy b/protocol/contracts/external/Gauge.vy new file mode 100644 index 0000000..42e764f --- /dev/null +++ b/protocol/contracts/external/Gauge.vy @@ -0,0 +1,312 @@ +# @version 0.2.7 +""" +@title Liquidity Gauge +@author Curve Finance +@license MIT +@notice Used for measuring liquidity and insurance +""" + +from vyper.interfaces import ERC20 + +interface CRV20: + def future_epoch_time_write() -> uint256: nonpayable + def rate() -> uint256: view + +interface Controller: + def period() -> int128: view + def period_write() -> int128: nonpayable + def period_timestamp(p: int128) -> uint256: view + def gauge_relative_weight(addr: address, time: uint256) -> uint256: view + def voting_escrow() -> address: view + def checkpoint(): nonpayable + def checkpoint_gauge(addr: address): nonpayable + +interface Minter: + def token() -> address: view + def controller() -> address: view + def minted(user: address, gauge: address) -> uint256: view + +interface VotingEscrow: + def user_point_epoch(addr: address) -> uint256: view + def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: address + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + + +TOKENLESS_PRODUCTION: constant(uint256) = 40 +BOOST_WARMUP: constant(uint256) = 2 * 7 * 86400 +WEEK: constant(uint256) = 604800 + +minter: public(address) +crv_token: public(address) +lp_token: public(address) +controller: public(address) +voting_escrow: public(address) +balanceOf: public(HashMap[address, uint256]) +totalSupply: public(uint256) +future_epoch_time: public(uint256) + +# caller -> recipient -> can deposit? +approved_to_deposit: public(HashMap[address, HashMap[address, bool]]) + +working_balances: public(HashMap[address, uint256]) +working_supply: public(uint256) + +# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint +# All values are kept in units of being multiplied by 1e18 +period: public(int128) +period_timestamp: public(uint256[100000000000000000000000000000]) + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint +integrate_inv_supply: public(uint256[100000000000000000000000000000]) # bump epoch when rate() changes + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint +integrate_inv_supply_of: public(HashMap[address, uint256]) +integrate_checkpoint_of: public(HashMap[address, uint256]) + + +# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint +# Units: rate * t = already number of coins per address to issue +integrate_fraction: public(HashMap[address, uint256]) + +inflation_rate: public(uint256) + + +@external +def __init__(lp_addr: address, _minter: address): + """ + @notice Contract constructor + @param lp_addr Liquidity Pool contract address + @param _minter Minter contract address + """ + + assert lp_addr != ZERO_ADDRESS + assert _minter != ZERO_ADDRESS + + self.lp_token = lp_addr + self.minter = _minter + crv_addr: address = Minter(_minter).token() + self.crv_token = crv_addr + controller_addr: address = Minter(_minter).controller() + self.controller = controller_addr + self.voting_escrow = Controller(controller_addr).voting_escrow() + self.period_timestamp[0] = block.timestamp + self.inflation_rate = CRV20(crv_addr).rate() + self.future_epoch_time = CRV20(crv_addr).future_epoch_time_write() + + +@internal +def _update_liquidity_limit(addr: address, l: uint256, L: uint256): + """ + @notice Calculate limits which depend on the amount of CRV token per-user. + Effectively it calculates working balances to apply amplification + of CRV production by CRV + @param addr User address + @param l User's amount of liquidity (LP tokens) + @param L Total amount of liquidity (LP tokens) + """ + # To be called after totalSupply is updated + _voting_escrow: address = self.voting_escrow + voting_balance: uint256 = ERC20(_voting_escrow).balanceOf(addr) + voting_total: uint256 = ERC20(_voting_escrow).totalSupply() + + lim: uint256 = l * TOKENLESS_PRODUCTION / 100 + if (voting_total > 0) and (block.timestamp > self.period_timestamp[0] + BOOST_WARMUP): + lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 + + lim = min(l, lim) + old_bal: uint256 = self.working_balances[addr] + self.working_balances[addr] = lim + _working_supply: uint256 = self.working_supply + lim - old_bal + self.working_supply = _working_supply + + log UpdateLiquidityLimit(addr, l, L, lim, _working_supply) + + +@internal +def _checkpoint(addr: address): + """ + @notice Checkpoint for a user + @param addr User address + """ + _token: address = self.crv_token + _controller: address = self.controller + _period: int128 = self.period + _period_time: uint256 = self.period_timestamp[_period] + _integrate_inv_supply: uint256 = self.integrate_inv_supply[_period] + rate: uint256 = self.inflation_rate + new_rate: uint256 = rate + prev_future_epoch: uint256 = self.future_epoch_time + if prev_future_epoch >= _period_time: + self.future_epoch_time = CRV20(_token).future_epoch_time_write() + new_rate = CRV20(_token).rate() + self.inflation_rate = new_rate + Controller(_controller).checkpoint_gauge(self) + + _working_balance: uint256 = self.working_balances[addr] + _working_supply: uint256 = self.working_supply + + # Update integral of 1/supply + if block.timestamp > _period_time: + prev_week_time: uint256 = _period_time + week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp) + + for i in range(500): + dt: uint256 = week_time - prev_week_time + w: uint256 = Controller(_controller).gauge_relative_weight(self, prev_week_time / WEEK * WEEK) + + if _working_supply > 0: + if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time: + # If we went across one or multiple epochs, apply the rate + # of the first epoch until it ends, and then the rate of + # the last epoch. + # If more than one epoch is crossed - the gauge gets less, + # but that'd meen it wasn't called for more than 1 year + _integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply + rate = new_rate + _integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply + else: + _integrate_inv_supply += rate * w * dt / _working_supply + # On precisions of the calculation + # rate ~= 10e18 + # last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) + # _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) + # The largest loss is at dt = 1 + # Loss is 1e-9 - acceptable + + if week_time == block.timestamp: + break + prev_week_time = week_time + week_time = min(week_time + WEEK, block.timestamp) + + _period += 1 + self.period = _period + self.period_timestamp[_period] = block.timestamp + self.integrate_inv_supply[_period] = _integrate_inv_supply + + # Update user-specific integrals + self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18 + self.integrate_inv_supply_of[addr] = _integrate_inv_supply + self.integrate_checkpoint_of[addr] = block.timestamp + + +@external +def user_checkpoint(addr: address) -> bool: + """ + @notice Record a checkpoint for `addr` + @param addr User address + @return bool success + """ + assert (msg.sender == addr) or (msg.sender == self.minter) # dev: unauthorized + self._checkpoint(addr) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + return True + + +@external +def claimable_tokens(addr: address) -> uint256: + """ + @notice Get the number of claimable tokens per user + @dev This function should be manually changed to "view" in the ABI + @return uint256 number of claimable tokens per user + """ + self._checkpoint(addr) + return self.integrate_fraction[addr] - Minter(self.minter).minted(addr, self) + + +@external +def kick(addr: address): + """ + @notice Kick `addr` for abusing their boost + @dev Only if either they had another voting event, or their voting escrow lock expired + @param addr Address to kick + """ + _voting_escrow: address = self.voting_escrow + t_last: uint256 = self.integrate_checkpoint_of[addr] + t_ve: uint256 = VotingEscrow(_voting_escrow).user_point_history__ts( + addr, VotingEscrow(_voting_escrow).user_point_epoch(addr) + ) + _balance: uint256 = self.balanceOf[addr] + + assert ERC20(self.voting_escrow).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed + assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed + + self._checkpoint(addr) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + + +@external +def set_approve_deposit(addr: address, can_deposit: bool): + """ + @notice Set whether `addr` can deposit tokens for `msg.sender` + @param addr Address to set approval on + @param can_deposit bool - can this account deposit for `msg.sender`? + """ + self.approved_to_deposit[addr][msg.sender] = can_deposit + + +@external +@nonreentrant('lock') +def deposit(_value: uint256, addr: address = msg.sender): + """ + @notice Deposit `_value` LP tokens + @param _value Number of tokens to deposit + @param addr Address to deposit for + """ + if addr != msg.sender: + assert self.approved_to_deposit[msg.sender][addr], "Not approved" + + self._checkpoint(addr) + + if _value != 0: + _balance: uint256 = self.balanceOf[addr] + _value + _supply: uint256 = self.totalSupply + _value + self.balanceOf[addr] = _balance + self.totalSupply = _supply + + self._update_liquidity_limit(addr, _balance, _supply) + + assert ERC20(self.lp_token).transferFrom(msg.sender, self, _value) + + log Deposit(addr, _value) + + +@external +@nonreentrant('lock') +def withdraw(_value: uint256): + """ + @notice Withdraw `_value` LP tokens + @param _value Number of tokens to withdraw + """ + self._checkpoint(msg.sender) + + _balance: uint256 = self.balanceOf[msg.sender] - _value + _supply: uint256 = self.totalSupply - _value + self.balanceOf[msg.sender] = _balance + self.totalSupply = _supply + + self._update_liquidity_limit(msg.sender, _balance, _supply) + + assert ERC20(self.lp_token).transfer(msg.sender, _value) + + log Withdraw(msg.sender, _value) + + +@external +@view +def integrate_checkpoint() -> uint256: + return self.period_timestamp[self.period] \ No newline at end of file diff --git a/protocol/contracts/external/TokenMiner.vy b/protocol/contracts/external/TokenMiner.vy new file mode 100644 index 0000000..d91ef38 --- /dev/null +++ b/protocol/contracts/external/TokenMiner.vy @@ -0,0 +1,99 @@ +# @version 0.2.7 +""" +@title Token Minter +@author Curve Finance +@license MIT +""" + +interface LiquidityGauge: + # Presumably, other gauges will provide the same interfaces + def integrate_fraction(addr: address) -> uint256: view + def user_checkpoint(addr: address) -> bool: nonpayable + +interface MERC20: + def mint(_to: address, _value: uint256) -> bool: nonpayable + +interface GaugeController: + def gauge_types(addr: address) -> int128: view + + +event Minted: + recipient: indexed(address) + gauge: address + minted: uint256 + + +token: public(address) +controller: public(address) + +# user -> gauge -> value +minted: public(HashMap[address, HashMap[address, uint256]]) + +# minter -> user -> can mint? +allowed_to_mint_for: public(HashMap[address, HashMap[address, bool]]) + + +@external +def __init__(_token: address, _controller: address): + self.token = _token + self.controller = _controller + + +@internal +def _mint_for(gauge_addr: address, _for: address): + assert GaugeController(self.controller).gauge_types(gauge_addr) >= 0 # dev: gauge is not added + + LiquidityGauge(gauge_addr).user_checkpoint(_for) + total_mint: uint256 = LiquidityGauge(gauge_addr).integrate_fraction(_for) + to_mint: uint256 = total_mint - self.minted[_for][gauge_addr] + + if to_mint != 0: + MERC20(self.token).mint(_for, to_mint) + self.minted[_for][gauge_addr] = total_mint + + log Minted(_for, gauge_addr, total_mint) + + +@external +@nonreentrant('lock') +def mint(gauge_addr: address): + """ + @notice Mint everything which belongs to `msg.sender` and send to them + @param gauge_addr `LiquidityGauge` address to get mintable amount from + """ + self._mint_for(gauge_addr, msg.sender) + + +@external +@nonreentrant('lock') +def mint_many(gauge_addrs: address[8]): + """ + @notice Mint everything which belongs to `msg.sender` across multiple gauges + @param gauge_addrs List of `LiquidityGauge` addresses + """ + for i in range(8): + if gauge_addrs[i] == ZERO_ADDRESS: + break + self._mint_for(gauge_addrs[i], msg.sender) + + +@external +@nonreentrant('lock') +def mint_for(gauge_addr: address, _for: address): + """ + @notice Mint tokens for `_for` + @dev Only possible when `msg.sender` has been approved via `toggle_approve_mint` + @param gauge_addr `LiquidityGauge` address to get mintable amount from + @param _for Address to mint to + """ + if self.allowed_to_mint_for[msg.sender][_for]: + self._mint_for(gauge_addr, _for) + + +@external +def toggle_approve_mint(minting_user: address): + """ + @notice allow `minting_user` to mint for `msg.sender` + @param minting_user Address to toggle permission for + """ + self.allowed_to_mint_for[minting_user][msg.sender] = not self.allowed_to_mint_for[minting_user][msg.sender] \ No newline at end of file diff --git a/protocol/contracts/gnosis-safe-contracts/GnosisSafe.sol b/protocol/contracts/gnosis-safe-contracts/GnosisSafe.sol new file mode 100644 index 0000000..e3b6d21 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/GnosisSafe.sol @@ -0,0 +1,512 @@ +pragma solidity 0.5.17; +import "./base/ModuleManager.sol"; +import "./base/OwnerManager.sol"; +import "./base/FallbackManager.sol"; +import "./common/MasterCopy.sol"; +import "./common/SignatureDecoder.sol"; +import "./common/SecuredTokenTransfer.sol"; +import "./interfaces/ISignatureValidator.sol"; +import "./external/GnosisSafeMath.sol"; + +/// @title Gnosis Safe - A multisignature wallet with support for confirmations using signed messages based on ERC191. +/// @author Stefan George - +/// @author Richard Meissner - +/// @author Ricardo Guilherme Schmidt - (Status Research & Development GmbH) - Gas Token Payment +contract GnosisSafe is + MasterCopy, + ModuleManager, + OwnerManager, + SignatureDecoder, + SecuredTokenTransfer, + ISignatureValidatorConstants, + FallbackManager +{ + using GnosisSafeMath for uint256; + + string public constant NAME = "Gnosis Safe"; + string public constant VERSION = "1.2.0"; + + //keccak256( + // "EIP712Domain(address verifyingContract)" + //); + bytes32 + private constant DOMAIN_SEPARATOR_TYPEHASH = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; + + //keccak256( + // "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)" + //); + bytes32 + private constant SAFE_TX_TYPEHASH = 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8; + + //keccak256( + // "SafeMessage(bytes message)" + //); + bytes32 + private constant SAFE_MSG_TYPEHASH = 0x60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1146b5a6150fbca; + + event ApproveHash(bytes32 indexed approvedHash, address indexed owner); + event SignMsg(bytes32 indexed msgHash); + event ExecutionFailure(bytes32 txHash, uint256 payment); + event ExecutionSuccess(bytes32 txHash, uint256 payment); + + uint256 public nonce; + bytes32 public domainSeparator; + // Mapping to keep track of all message hashes that have been approve by ALL REQUIRED owners + mapping(bytes32 => uint256) public signedMessages; + // Mapping to keep track of all hashes (message or transaction) that have been approve by ANY owners + mapping(address => mapping(bytes32 => uint256)) public approvedHashes; + + // This constructor ensures that this contract can only be used as a master copy for Proxy contracts + constructor() public { + // By setting the threshold it is not possible to call setup anymore, + // so we create a Safe with 0 owners and threshold 1. + // This is an unusable Safe, perfect for the mastercopy + threshold = 1; + } + + /// @dev Setup function sets initial storage of contract. + /// @param _owners List of Safe owners. + /// @param _threshold Number of required confirmations for a Safe transaction. + /// @param to Contract address for optional delegate call. + /// @param data Data payload for optional delegate call. + /// @param fallbackHandler Handler for fallback calls to this contract + /// @param paymentToken Token that should be used for the payment (0 is ETH) + /// @param payment Value that should be paid + /// @param paymentReceiver Adddress that should receive the payment (or 0 if tx.origin) + function setup( + address[] calldata _owners, + uint256 _threshold, + address to, + bytes calldata data, + address fallbackHandler, + address paymentToken, + uint256 payment, + address payable paymentReceiver + ) external { + require(domainSeparator == 0, "Domain Separator already set!"); + domainSeparator = keccak256( + abi.encode(DOMAIN_SEPARATOR_TYPEHASH, this) + ); + setupOwners(_owners, _threshold); + if (fallbackHandler != address(0)) + internalSetFallbackHandler(fallbackHandler); + // As setupOwners can only be called if the contract has not been initialized we don't need a check for setupModules + setupModules(to, data); + + if (payment > 0) { + // To avoid running into issues with EIP-170 we reuse the handlePayment function (to avoid adjusting code of that has been verified we do not adjust the method itself) + // baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment + handlePayment(payment, 0, 1, paymentToken, paymentReceiver); + } + } + + /// @dev Allows to execute a Safe transaction confirmed by required number of owners and then pays the account that submitted the transaction. + /// Note: The fees are always transfered, even if the user transaction fails. + /// @param to Destination address of Safe transaction. + /// @param value Ether value of Safe transaction. + /// @param data Data payload of Safe transaction. + /// @param operation Operation type of Safe transaction. + /// @param safeTxGas Gas that should be used for the Safe transaction. + /// @param baseGas Gas costs for that are indipendent of the transaction execution(e.g. base transaction fee, signature check, payment of the refund) + /// @param gasPrice Gas price that should be used for the payment calculation. + /// @param gasToken Token address (or 0 if ETH) that is used for the payment. + /// @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin). + /// @param signatures Packed signature data ({bytes32 r}{bytes32 s}{uint8 v}) + function execTransaction( + address to, + uint256 value, + bytes calldata data, + Enum.Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address payable refundReceiver, + bytes calldata signatures + ) external payable returns (bool success) { + bytes32 txHash; + // Use scope here to limit variable lifetime and prevent `stack too deep` errors + { + bytes memory txHashData = encodeTransactionData( + to, + value, + data, + operation, // Transaction info + safeTxGas, + baseGas, + gasPrice, + gasToken, + refundReceiver, // Payment info + nonce + ); + // Increase nonce and execute transaction. + nonce++; + txHash = keccak256(txHashData); + checkSignatures(txHash, txHashData, signatures, true); + } + // We require some gas to emit the events (at least 2500) after the execution and some to perform code until the execution (500) + // We also include the 1/64 in the check that is not send along with a call to counteract potential shortings because of EIP-150 + require( + gasleft() >= ((safeTxGas * 64) / 63).max(safeTxGas + 2500) + 500, + "Not enough gas to execute safe transaction" + ); + // Use scope here to limit variable lifetime and prevent `stack too deep` errors + { + uint256 gasUsed = gasleft(); + // If the gasPrice is 0 we assume that nearly all available gas can be used (it is always more than safeTxGas) + // We only substract 2500 (compared to the 3000 before) to ensure that the amount passed is still higher than safeTxGas + success = execute( + to, + value, + data, + operation, + gasPrice == 0 ? (gasleft() - 2500) : safeTxGas + ); + gasUsed = gasUsed.sub(gasleft()); + // We transfer the calculated tx costs to the tx.origin to avoid sending it to intermediate contracts that have made calls + uint256 payment = 0; + if (gasPrice > 0) { + payment = handlePayment( + gasUsed, + baseGas, + gasPrice, + gasToken, + refundReceiver + ); + } + if (success) emit ExecutionSuccess(txHash, payment); + else emit ExecutionFailure(txHash, payment); + } + } + + function handlePayment( + uint256 gasUsed, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address payable refundReceiver + ) private returns (uint256 payment) { + // solium-disable-next-line security/no-tx-origin + address payable receiver = refundReceiver == address(0) + ? tx.origin + : refundReceiver; + if (gasToken == address(0)) { + // For ETH we will only adjust the gas price to not be higher than the actual used gas price + payment = gasUsed.add(baseGas).mul( + gasPrice < tx.gasprice ? gasPrice : tx.gasprice + ); + // solium-disable-next-line security/no-send + require( + receiver.send(payment), + "Could not pay gas costs with ether" + ); + } else { + payment = gasUsed.add(baseGas).mul(gasPrice); + require( + transferToken(gasToken, receiver, payment), + "Could not pay gas costs with token" + ); + } + } + + /** + * @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise. + * @param dataHash Hash of the data (could be either a message hash or transaction hash) + * @param data That should be signed (this is passed to an external validator contract) + * @param signatures Signature data that should be verified. Can be ECDSA signature, contract signature (EIP-1271) or approved hash. + * @param consumeHash Indicates that in case of an approved hash the storage can be freed to save gas + */ + function checkSignatures( + bytes32 dataHash, + bytes memory data, + bytes memory signatures, + bool consumeHash + ) internal { + // Load threshold to avoid multiple storage loads + uint256 _threshold = threshold; + // Check that a threshold is set + require(_threshold > 0, "Threshold needs to be defined!"); + // Check that the provided signature data is not too short + require( + signatures.length >= _threshold.mul(65), + "Signatures data too short" + ); + // There cannot be an owner with address 0. + address lastOwner = address(0); + address currentOwner; + uint8 v; + bytes32 r; + bytes32 s; + uint256 i; + for (i = 0; i < _threshold; i++) { + (v, r, s) = signatureSplit(signatures, i); + // If v is 0 then it is a contract signature + if (v == 0) { + // When handling contract signatures the address of the contract is encoded into r + currentOwner = address(uint256(r)); + + // Check that signature data pointer (s) is not pointing inside the static part of the signatures bytes + // This check is not completely accurate, since it is possible that more signatures than the threshold are send. + // Here we only check that the pointer is not pointing inside the part that is being processed + require( + uint256(s) >= _threshold.mul(65), + "Invalid contract signature location: inside static part" + ); + + // Check that signature data pointer (s) is in bounds (points to the length of data -> 32 bytes) + require( + uint256(s).add(32) <= signatures.length, + "Invalid contract signature location: length not present" + ); + + // Check if the contract signature is in bounds: start of data is s + 32 and end is start + signature length + uint256 contractSignatureLen; + // solium-disable-next-line security/no-inline-assembly + assembly { + contractSignatureLen := mload(add(add(signatures, s), 0x20)) + } + require( + uint256(s).add(32).add(contractSignatureLen) <= + signatures.length, + "Invalid contract signature location: data not complete" + ); + + // Check signature + bytes memory contractSignature; + // solium-disable-next-line security/no-inline-assembly + assembly { + // The signature data for contract signatures is appended to the concatenated signatures and the offset is stored in s + contractSignature := add(add(signatures, s), 0x20) + } + require( + ISignatureValidator(currentOwner).isValidSignature( + data, + contractSignature + ) == EIP1271_MAGIC_VALUE, + "Invalid contract signature provided" + ); + // If v is 1 then it is an approved hash + } else if (v == 1) { + // When handling approved hashes the address of the approver is encoded into r + currentOwner = address(uint256(r)); + // Hashes are automatically approved by the sender of the message or when they have been pre-approved via a separate transaction + require( + msg.sender == currentOwner || + approvedHashes[currentOwner][dataHash] != 0, + "Hash has not been approved" + ); + // Hash has been marked for consumption. If this hash was pre-approved free storage + if (consumeHash && msg.sender != currentOwner) { + approvedHashes[currentOwner][dataHash] = 0; + } + } else if (v > 30) { + // To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before applying ecrecover + currentOwner = ecrecover( + keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + dataHash + ) + ), + v - 4, + r, + s + ); + } else { + // Use ecrecover with the messageHash for EOA signatures + currentOwner = ecrecover(dataHash, v, r, s); + } + require( + currentOwner > lastOwner && + owners[currentOwner] != address(0) && + currentOwner != SENTINEL_OWNERS, + "Invalid owner provided" + ); + lastOwner = currentOwner; + } + } + + /// @dev Allows to estimate a Safe transaction. + /// This method is only meant for estimation purpose, therefore two different protection mechanism against execution in a transaction have been made: + /// 1.) The method can only be called from the safe itself + /// 2.) The response is returned with a revert + /// When estimating set `from` to the address of the safe. + /// Since the `estimateGas` function includes refunds, call this method to get an estimated of the costs that are deducted from the safe with `execTransaction` + /// @param to Destination address of Safe transaction. + /// @param value Ether value of Safe transaction. + /// @param data Data payload of Safe transaction. + /// @param operation Operation type of Safe transaction. + /// @return Estimate without refunds and overhead fees (base transaction and payload data gas costs). + function requiredTxGas( + address to, + uint256 value, + bytes calldata data, + Enum.Operation operation + ) external authorized returns (uint256) { + uint256 startGas = gasleft(); + // We don't provide an error message here, as we use it to return the estimate + // solium-disable-next-line error-reason + require(execute(to, value, data, operation, gasleft())); + uint256 requiredGas = startGas - gasleft(); + // Convert response to string and return via error message + revert(string(abi.encodePacked(requiredGas))); + } + + /** + * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. + * @param hashToApprove The hash that should be marked as approved for signatures that are verified by this contract. + */ + function approveHash(bytes32 hashToApprove) external { + require( + owners[msg.sender] != address(0), + "Only owners can approve a hash" + ); + approvedHashes[msg.sender][hashToApprove] = 1; + emit ApproveHash(hashToApprove, msg.sender); + } + + /** + * @dev Marks a message as signed, so that it can be used with EIP-1271 + * @notice Marks a message (`_data`) as signed. + * @param _data Arbitrary length data that should be marked as signed on the behalf of address(this) + */ + function signMessage(bytes calldata _data) external authorized { + bytes32 msgHash = getMessageHash(_data); + signedMessages[msgHash] = 1; + emit SignMsg(msgHash); + } + + /** + * Implementation of ISignatureValidator (see `interfaces/ISignatureValidator.sol`) + * @dev Should return whether the signature provided is valid for the provided data. + * The save does not implement the interface since `checkSignatures` is not a view method. + * The method will not perform any state changes (see parameters of `checkSignatures`) + * @param _data Arbitrary length data signed on the behalf of address(this) + * @param _signature Signature byte array associated with _data + * @return a bool upon valid or invalid signature with corresponding _data + */ + function isValidSignature(bytes calldata _data, bytes calldata _signature) + external + returns (bytes4) + { + bytes32 messageHash = getMessageHash(_data); + if (_signature.length == 0) { + require(signedMessages[messageHash] != 0, "Hash not approved"); + } else { + // consumeHash needs to be false, as the state should not be changed + checkSignatures(messageHash, _data, _signature, false); + } + return EIP1271_MAGIC_VALUE; + } + + /// @dev Returns hash of a message that can be signed by owners. + /// @param message Message that should be hashed + /// @return Message hash. + function getMessageHash(bytes memory message) + public + view + returns (bytes32) + { + bytes32 safeMessageHash = keccak256( + abi.encode(SAFE_MSG_TYPEHASH, keccak256(message)) + ); + return + keccak256( + abi.encodePacked( + bytes1(0x19), + bytes1(0x01), + domainSeparator, + safeMessageHash + ) + ); + } + + /// @dev Returns the bytes that are hashed to be signed by owners. + /// @param to Destination address. + /// @param value Ether value. + /// @param data Data payload. + /// @param operation Operation type. + /// @param safeTxGas Fas that should be used for the safe transaction. + /// @param baseGas Gas costs for data used to trigger the safe transaction. + /// @param gasPrice Maximum gas price that should be used for this transaction. + /// @param gasToken Token address (or 0 if ETH) that is used for the payment. + /// @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin). + /// @param _nonce Transaction nonce. + /// @return Transaction hash bytes. + function encodeTransactionData( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address refundReceiver, + uint256 _nonce + ) public view returns (bytes memory) { + bytes32 safeTxHash = keccak256( + abi.encode( + SAFE_TX_TYPEHASH, + to, + value, + keccak256(data), + operation, + safeTxGas, + baseGas, + gasPrice, + gasToken, + refundReceiver, + _nonce + ) + ); + return + abi.encodePacked( + bytes1(0x19), + bytes1(0x01), + domainSeparator, + safeTxHash + ); + } + + /// @dev Returns hash to be signed by owners. + /// @param to Destination address. + /// @param value Ether value. + /// @param data Data payload. + /// @param operation Operation type. + /// @param safeTxGas Fas that should be used for the safe transaction. + /// @param baseGas Gas costs for data used to trigger the safe transaction. + /// @param gasPrice Maximum gas price that should be used for this transaction. + /// @param gasToken Token address (or 0 if ETH) that is used for the payment. + /// @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin). + /// @param _nonce Transaction nonce. + /// @return Transaction hash. + function getTransactionHash( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address refundReceiver, + uint256 _nonce + ) public view returns (bytes32) { + return + keccak256( + encodeTransactionData( + to, + value, + data, + operation, + safeTxGas, + baseGas, + gasPrice, + gasToken, + refundReceiver, + _nonce + ) + ); + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/base/Executor.sol b/protocol/contracts/gnosis-safe-contracts/base/Executor.sol new file mode 100644 index 0000000..186cf7d --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/base/Executor.sol @@ -0,0 +1,58 @@ +pragma solidity 0.5.17; +import "../common/Enum.sol"; + +/// @title Executor - A contract that can execute transactions +/// @author Richard Meissner - +contract Executor { + function execute( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation, + uint256 txGas + ) internal returns (bool success) { + if (operation == Enum.Operation.Call) + success = executeCall(to, value, data, txGas); + else if (operation == Enum.Operation.DelegateCall) + success = executeDelegateCall(to, data, txGas); + else success = false; + } + + function executeCall( + address to, + uint256 value, + bytes memory data, + uint256 txGas + ) internal returns (bool success) { + // solium-disable-next-line security/no-inline-assembly + assembly { + success := call( + txGas, + to, + value, + add(data, 0x20), + mload(data), + 0, + 0 + ) + } + } + + function executeDelegateCall( + address to, + bytes memory data, + uint256 txGas + ) internal returns (bool success) { + // solium-disable-next-line security/no-inline-assembly + assembly { + success := delegatecall( + txGas, + to, + add(data, 0x20), + mload(data), + 0, + 0 + ) + } + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/base/FallbackManager.sol b/protocol/contracts/gnosis-safe-contracts/base/FallbackManager.sol new file mode 100644 index 0000000..1611aee --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/base/FallbackManager.sol @@ -0,0 +1,53 @@ +pragma solidity 0.5.17; + +import "../common/SelfAuthorized.sol"; + +/// @title Fallback Manager - A contract that manages fallback calls made to this contract +/// @author Richard Meissner - +contract FallbackManager is SelfAuthorized { + // keccak256("fallback_manager.handler.address") + bytes32 + internal constant FALLBACK_HANDLER_STORAGE_SLOT = 0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5; + + function internalSetFallbackHandler(address handler) internal { + bytes32 slot = FALLBACK_HANDLER_STORAGE_SLOT; + // solium-disable-next-line security/no-inline-assembly + assembly { + sstore(slot, handler) + } + } + + /// @dev Allows to add a contract to handle fallback calls. + /// Only fallback calls without value and with data will be forwarded. + /// This can only be done via a Safe transaction. + /// @param handler contract to handle fallbacks calls. + function setFallbackHandler(address handler) public authorized { + internalSetFallbackHandler(handler); + } + + function() external payable { + // Only calls without value and with data will be forwarded + if (msg.value > 0 || msg.data.length == 0) { + return; + } + bytes32 slot = FALLBACK_HANDLER_STORAGE_SLOT; + address handler; + // solium-disable-next-line security/no-inline-assembly + assembly { + handler := sload(slot) + } + + if (handler != address(0)) { + // solium-disable-next-line security/no-inline-assembly + assembly { + calldatacopy(0, 0, calldatasize()) + let success := call(gas, handler, 0, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + if eq(success, 0) { + revert(0, returndatasize()) + } + return(0, returndatasize()) + } + } + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/base/Module.sol b/protocol/contracts/gnosis-safe-contracts/base/Module.sol new file mode 100644 index 0000000..c849d13 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/base/Module.sol @@ -0,0 +1,25 @@ +pragma solidity 0.5.17; +import "../common/MasterCopy.sol"; +import "./ModuleManager.sol"; + +/// @title Module - Base class for modules. +/// @author Stefan George - +/// @author Richard Meissner - +contract Module is MasterCopy { + ModuleManager public manager; + + modifier authorized() { + require( + msg.sender == address(manager), + "Method can only be called from manager" + ); + _; + } + + function setManager() internal { + // manager can only be 0 at initalization of contract. + // Check ensures that setup function can only be called once. + require(address(manager) == address(0), "Manager has already been set"); + manager = ModuleManager(msg.sender); + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/base/ModuleManager.sol b/protocol/contracts/gnosis-safe-contracts/base/ModuleManager.sol new file mode 100644 index 0000000..aab28da --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/base/ModuleManager.sol @@ -0,0 +1,157 @@ +pragma solidity 0.5.17; +import "../common/Enum.sol"; +import "../common/SelfAuthorized.sol"; +import "./Executor.sol"; +import "./Module.sol"; + + +/// @title Module Manager - A contract that manages modules that can execute transactions via this contract +/// @author Stefan George - +/// @author Richard Meissner - +contract ModuleManager is SelfAuthorized, Executor { + + event EnabledModule(Module module); + event DisabledModule(Module module); + event ExecutionFromModuleSuccess(address indexed module); + event ExecutionFromModuleFailure(address indexed module); + + address internal constant SENTINEL_MODULES = address(0x1); + + mapping (address => address) internal modules; + + function setupModules(address to, bytes memory data) + internal + { + require(modules[SENTINEL_MODULES] == address(0), "Modules have already been initialized"); + modules[SENTINEL_MODULES] = SENTINEL_MODULES; + if (to != address(0)) + // Setup has to complete successfully or transaction fails. + require(executeDelegateCall(to, data, gasleft()), "Could not finish initialization"); + } + + /// @dev Allows to add a module to the whitelist. + /// This can only be done via a Safe transaction. + /// @notice Enables the module `module` for the Safe. + /// @param module Module to be whitelisted. + function enableModule(Module module) + public + authorized + { + // Module address cannot be null or sentinel. + require(address(module) != address(0) && address(module) != SENTINEL_MODULES, "Invalid module address provided"); + // Module cannot be added twice. + require(modules[address(module)] == address(0), "Module has already been added"); + modules[address(module)] = modules[SENTINEL_MODULES]; + modules[SENTINEL_MODULES] = address(module); + emit EnabledModule(module); + } + + /// @dev Allows to remove a module from the whitelist. + /// This can only be done via a Safe transaction. + /// @notice Disables the module `module` for the Safe. + /// @param prevModule Module that pointed to the module to be removed in the linked list + /// @param module Module to be removed. + function disableModule(Module prevModule, Module module) + public + authorized + { + // Validate module address and check that it corresponds to module index. + require(address(module) != address(0) && address(module) != SENTINEL_MODULES, "Invalid module address provided"); + require(modules[address(prevModule)] == address(module), "Invalid prevModule, module pair provided"); + modules[address(prevModule)] = modules[address(module)]; + modules[address(module)] = address(0); + emit DisabledModule(module); + } + + /// @dev Allows a Module to execute a Safe transaction without any further confirmations. + /// @param to Destination address of module transaction. + /// @param value Ether value of module transaction. + /// @param data Data payload of module transaction. + /// @param operation Operation type of module transaction. + function execTransactionFromModule(address to, uint256 value, bytes memory data, Enum.Operation operation) + public + returns (bool success) + { + // Only whitelisted modules are allowed. + require(msg.sender != SENTINEL_MODULES && modules[msg.sender] != address(0), "Method can only be called from an enabled module"); + // Execute transaction without further confirmations. + success = execute(to, value, data, operation, gasleft()); + if (success) emit ExecutionFromModuleSuccess(msg.sender); + else emit ExecutionFromModuleFailure(msg.sender); + } + + /// @dev Allows a Module to execute a Safe transaction without any further confirmations and return data + /// @param to Destination address of module transaction. + /// @param value Ether value of module transaction. + /// @param data Data payload of module transaction. + /// @param operation Operation type of module transaction. + function execTransactionFromModuleReturnData(address to, uint256 value, bytes memory data, Enum.Operation operation) + public + returns (bool success, bytes memory returnData) + { + success = execTransactionFromModule(to, value, data, operation); + // solium-disable-next-line security/no-inline-assembly + assembly { + // Load free memory location + let ptr := mload(0x40) + // We allocate memory for the return data by setting the free memory location to + // current free memory location + data size + 32 bytes for data size value + mstore(0x40, add(ptr, add(returndatasize(), 0x20))) + // Store the size + mstore(ptr, returndatasize()) + // Store the data + returndatacopy(add(ptr, 0x20), 0, returndatasize()) + // Point the return data to the correct memory location + returnData := ptr + } + } + + /// @dev Returns if an module is enabled + /// @return True if the module is enabled + function isModuleEnabled(Module module) + public + view + returns (bool) + { + return SENTINEL_MODULES != address(module) && modules[address(module)] != address(0); + } + + /// @dev Returns array of first 10 modules. + /// @return Array of modules. + function getModules() + public + view + returns (address[] memory) + { + (address[] memory array,) = getModulesPaginated(SENTINEL_MODULES, 10); + return array; + } + + /// @dev Returns array of modules. + /// @param start Start of the page. + /// @param pageSize Maximum number of modules that should be returned. + /// @return Array of modules. + function getModulesPaginated(address start, uint256 pageSize) + public + view + returns (address[] memory array, address next) + { + // Init array with max page size + array = new address[](pageSize); + + // Populate return array + uint256 moduleCount = 0; + address currentModule = modules[start]; + while(currentModule != address(0x0) && currentModule != SENTINEL_MODULES && moduleCount < pageSize) { + array[moduleCount] = currentModule; + currentModule = modules[currentModule]; + moduleCount++; + } + next = currentModule; + // Set correct size of returned array + // solium-disable-next-line security/no-inline-assembly + assembly { + mstore(array, moduleCount) + } + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/base/OwnerManager.sol b/protocol/contracts/gnosis-safe-contracts/base/OwnerManager.sol new file mode 100644 index 0000000..097fc29 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/base/OwnerManager.sol @@ -0,0 +1,186 @@ +pragma solidity 0.5.17; +import "../common/SelfAuthorized.sol"; + +/// @title OwnerManager - Manages a set of owners and a threshold to perform actions. +/// @author Stefan George - +/// @author Richard Meissner - +contract OwnerManager is SelfAuthorized { + event AddedOwner(address owner); + event RemovedOwner(address owner); + event ChangedThreshold(uint256 threshold); + + address internal constant SENTINEL_OWNERS = address(0x1); + + mapping(address => address) internal owners; + uint256 ownerCount; + uint256 internal threshold; + + /// @dev Setup function sets initial storage of contract. + /// @param _owners List of Safe owners. + /// @param _threshold Number of required confirmations for a Safe transaction. + function setupOwners(address[] memory _owners, uint256 _threshold) + internal + { + // Threshold can only be 0 at initialization. + // Check ensures that setup function can only be called once. + require(threshold == 0, "Owners have already been setup"); + // Validate that threshold is smaller than number of added owners. + require( + _threshold <= _owners.length, + "Threshold cannot exceed owner count" + ); + // There has to be at least one Safe owner. + require(_threshold >= 1, "Threshold needs to be greater than 0"); + // Initializing Safe owners. + address currentOwner = SENTINEL_OWNERS; + for (uint256 i = 0; i < _owners.length; i++) { + // Owner address cannot be null. + address owner = _owners[i]; + require( + owner != address(0) && owner != SENTINEL_OWNERS, + "Invalid owner address provided" + ); + // No duplicate owners allowed. + require( + owners[owner] == address(0), + "Duplicate owner address provided" + ); + owners[currentOwner] = owner; + currentOwner = owner; + } + owners[currentOwner] = SENTINEL_OWNERS; + ownerCount = _owners.length; + threshold = _threshold; + } + + /// @dev Allows to add a new owner to the Safe and update the threshold at the same time. + /// This can only be done via a Safe transaction. + /// @notice Adds the owner `owner` to the Safe and updates the threshold to `_threshold`. + /// @param owner New owner address. + /// @param _threshold New threshold. + function addOwnerWithThreshold(address owner, uint256 _threshold) + public + authorized + { + // Owner address cannot be null. + require( + owner != address(0) && owner != SENTINEL_OWNERS, + "Invalid owner address provided" + ); + // No duplicate owners allowed. + require(owners[owner] == address(0), "Address is already an owner"); + owners[owner] = owners[SENTINEL_OWNERS]; + owners[SENTINEL_OWNERS] = owner; + ownerCount++; + emit AddedOwner(owner); + // Change threshold if threshold was changed. + if (threshold != _threshold) changeThreshold(_threshold); + } + + /// @dev Allows to remove an owner from the Safe and update the threshold at the same time. + /// This can only be done via a Safe transaction. + /// @notice Removes the owner `owner` from the Safe and updates the threshold to `_threshold`. + /// @param prevOwner Owner that pointed to the owner to be removed in the linked list + /// @param owner Owner address to be removed. + /// @param _threshold New threshold. + function removeOwner( + address prevOwner, + address owner, + uint256 _threshold + ) public authorized { + // Only allow to remove an owner, if threshold can still be reached. + require( + ownerCount - 1 >= _threshold, + "New owner count needs to be larger than new threshold" + ); + // Validate owner address and check that it corresponds to owner index. + require( + owner != address(0) && owner != SENTINEL_OWNERS, + "Invalid owner address provided" + ); + require( + owners[prevOwner] == owner, + "Invalid prevOwner, owner pair provided" + ); + owners[prevOwner] = owners[owner]; + owners[owner] = address(0); + ownerCount--; + emit RemovedOwner(owner); + // Change threshold if threshold was changed. + if (threshold != _threshold) changeThreshold(_threshold); + } + + /// @dev Allows to swap/replace an owner from the Safe with another address. + /// This can only be done via a Safe transaction. + /// @notice Replaces the owner `oldOwner` in the Safe with `newOwner`. + /// @param prevOwner Owner that pointed to the owner to be replaced in the linked list + /// @param oldOwner Owner address to be replaced. + /// @param newOwner New owner address. + function swapOwner( + address prevOwner, + address oldOwner, + address newOwner + ) public authorized { + // Owner address cannot be null. + require( + newOwner != address(0) && newOwner != SENTINEL_OWNERS, + "Invalid owner address provided" + ); + // No duplicate owners allowed. + require(owners[newOwner] == address(0), "Address is already an owner"); + // Validate oldOwner address and check that it corresponds to owner index. + require( + oldOwner != address(0) && oldOwner != SENTINEL_OWNERS, + "Invalid owner address provided" + ); + require( + owners[prevOwner] == oldOwner, + "Invalid prevOwner, owner pair provided" + ); + owners[newOwner] = owners[oldOwner]; + owners[prevOwner] = newOwner; + owners[oldOwner] = address(0); + emit RemovedOwner(oldOwner); + emit AddedOwner(newOwner); + } + + /// @dev Allows to update the number of required confirmations by Safe owners. + /// This can only be done via a Safe transaction. + /// @notice Changes the threshold of the Safe to `_threshold`. + /// @param _threshold New threshold. + function changeThreshold(uint256 _threshold) public authorized { + // Validate that threshold is smaller than number of owners. + require( + _threshold <= ownerCount, + "Threshold cannot exceed owner count" + ); + // There has to be at least one Safe owner. + require(_threshold >= 1, "Threshold needs to be greater than 0"); + threshold = _threshold; + emit ChangedThreshold(threshold); + } + + function getThreshold() public view returns (uint256) { + return threshold; + } + + function isOwner(address owner) public view returns (bool) { + return owner != SENTINEL_OWNERS && owners[owner] != address(0); + } + + /// @dev Returns array of owners. + /// @return Array of Safe owners. + function getOwners() public view returns (address[] memory) { + address[] memory array = new address[](ownerCount); + + // populate return array + uint256 index = 0; + address currentOwner = owners[SENTINEL_OWNERS]; + while (currentOwner != SENTINEL_OWNERS) { + array[index] = currentOwner; + currentOwner = owners[currentOwner]; + index++; + } + return array; + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/common/Enum.sol b/protocol/contracts/gnosis-safe-contracts/common/Enum.sol new file mode 100644 index 0000000..d3c677e --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/common/Enum.sol @@ -0,0 +1,7 @@ +pragma solidity 0.5.17; + +/// @title Enum - Collection of enums +/// @author Richard Meissner - +contract Enum { + enum Operation {Call, DelegateCall} +} diff --git a/protocol/contracts/gnosis-safe-contracts/common/EtherPaymentFallback.sol b/protocol/contracts/gnosis-safe-contracts/common/EtherPaymentFallback.sol new file mode 100644 index 0000000..1785eb1 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/common/EtherPaymentFallback.sol @@ -0,0 +1,8 @@ +pragma solidity ^0.5.0; + +/// @title EtherPaymentFallback - A contract that has a fallback to accept ether payments +/// @author Richard Meissner - +contract EtherPaymentFallback { + /// @dev Fallback function accepts Ether transactions. + function() external payable {} +} diff --git a/protocol/contracts/gnosis-safe-contracts/common/MasterCopy.sol b/protocol/contracts/gnosis-safe-contracts/common/MasterCopy.sol new file mode 100644 index 0000000..6109508 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/common/MasterCopy.sol @@ -0,0 +1,25 @@ +pragma solidity 0.5.17; +import "./SelfAuthorized.sol"; + +/// @title MasterCopy - Base for master copy contracts (should always be first super contract) +/// This contract is tightly coupled to our proxy contract (see `proxies/GnosisSafeProxy.sol`) +/// @author Richard Meissner - +contract MasterCopy is SelfAuthorized { + event ChangedMasterCopy(address masterCopy); + + // masterCopy always needs to be first declared variable, to ensure that it is at the same location as in the Proxy contract. + // It should also always be ensured that the address is stored alone (uses a full word) + address private masterCopy; + + /// @dev Allows to upgrade the contract. This can only be done via a Safe transaction. + /// @param _masterCopy New contract address. + function changeMasterCopy(address _masterCopy) public authorized { + // Master copy address cannot be null. + require( + _masterCopy != address(0), + "Invalid master copy address provided" + ); + masterCopy = _masterCopy; + emit ChangedMasterCopy(_masterCopy); + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/common/SecuredTokenTransfer.sol b/protocol/contracts/gnosis-safe-contracts/common/SecuredTokenTransfer.sol new file mode 100644 index 0000000..e74d5c0 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/common/SecuredTokenTransfer.sol @@ -0,0 +1,48 @@ +pragma solidity 0.5.17; + +/// @title SecuredTokenTransfer - Secure token transfer +/// @author Richard Meissner - +contract SecuredTokenTransfer { + /// @dev Transfers a token and returns if it was a success + /// @param token Token that should be transferred + /// @param receiver Receiver to whom the token should be transferred + /// @param amount The amount of tokens that should be transferred + function transferToken( + address token, + address receiver, + uint256 amount + ) internal returns (bool transferred) { + bytes memory data = abi.encodeWithSignature( + "transfer(address,uint256)", + receiver, + amount + ); + // solium-disable-next-line security/no-inline-assembly + assembly { + let success := call( + sub(gas, 10000), + token, + 0, + add(data, 0x20), + mload(data), + 0, + 0 + ) + let ptr := mload(0x40) + mstore(0x40, add(ptr, returndatasize())) + returndatacopy(ptr, 0, returndatasize()) + switch returndatasize() + case 0 { + transferred := success + } + case 0x20 { + transferred := iszero( + or(iszero(success), iszero(mload(ptr))) + ) + } + default { + transferred := 0 + } + } + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/common/SelfAuthorized.sol b/protocol/contracts/gnosis-safe-contracts/common/SelfAuthorized.sol new file mode 100644 index 0000000..0cf7b3b --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/common/SelfAuthorized.sol @@ -0,0 +1,13 @@ +pragma solidity 0.5.17; + +/// @title SelfAuthorized - authorizes current contract to perform actions +/// @author Richard Meissner - +contract SelfAuthorized { + modifier authorized() { + require( + msg.sender == address(this), + "Method can only be called from this contract" + ); + _; + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/common/SignatureDecoder.sol b/protocol/contracts/gnosis-safe-contracts/common/SignatureDecoder.sol new file mode 100644 index 0000000..d0c8846 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/common/SignatureDecoder.sol @@ -0,0 +1,52 @@ +pragma solidity 0.5.17; + +/// @title SignatureDecoder - Decodes signatures that a encoded as bytes +/// @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) +/// @author Richard Meissner - +contract SignatureDecoder { + /// @dev Recovers address who signed the message + /// @param messageHash operation ethereum signed message hash + /// @param messageSignature message `txHash` signature + /// @param pos which signature to read + function recoverKey( + bytes32 messageHash, + bytes memory messageSignature, + uint256 pos + ) internal pure returns (address) { + uint8 v; + bytes32 r; + bytes32 s; + (v, r, s) = signatureSplit(messageSignature, pos); + return ecrecover(messageHash, v, r, s); + } + + /// @dev divides bytes signature into `uint8 v, bytes32 r, bytes32 s`. + /// @notice Make sure to peform a bounds check for @param pos, to avoid out of bounds access on @param signatures + /// @param pos which signature to read. A prior bounds check of this parameter should be performed, to avoid out of bounds access + /// @param signatures concatenated rsv signatures + function signatureSplit(bytes memory signatures, uint256 pos) + internal + pure + returns ( + uint8 v, + bytes32 r, + bytes32 s + ) + { + // The signature format is a compact form of: + // {bytes32 r}{bytes32 s}{uint8 v} + // Compact means, uint8 is not padded to 32 bytes. + // solium-disable-next-line security/no-inline-assembly + assembly { + let signaturePos := mul(0x41, pos) + r := mload(add(signatures, add(signaturePos, 0x20))) + s := mload(add(signatures, add(signaturePos, 0x40))) + // Here we are loading the last 32 bytes, including 31 bytes + // of 's'. There is no 'mload8' to do this. + // + // 'byte' is not working due to the Solidity parser, so lets + // use the second best option, 'and' + v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff) + } + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/external/GnosisSafeMath.sol b/protocol/contracts/gnosis-safe-contracts/external/GnosisSafeMath.sol new file mode 100644 index 0000000..d682c2b --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/external/GnosisSafeMath.sol @@ -0,0 +1,73 @@ +pragma solidity 0.5.17; + +/** + * @title GnosisSafeMath + * @dev Math operations with safety checks that revert on error + * Renamed from SafeMath to GnosisSafeMath to avoid conflicts + * TODO: remove once open zeppelin update to solc 0.5.0 + */ +library GnosisSafeMath { + /** + * @dev Multiplies two numbers, reverts on overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b); + + return c; + } + + /** + * @dev Integer division of two numbers truncating the quotient, reverts on division by zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + require(b > 0); // Solidity only automatically asserts when dividing by 0 + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend). + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + require(b <= a); + uint256 c = a - b; + + return c; + } + + /** + * @dev Adds two numbers, reverts on overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a); + + return c; + } + + /** + * @dev Divides two numbers and returns the remainder (unsigned integer modulo), + * reverts when dividing by zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + require(b != 0); + return a % b; + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a >= b ? a : b; + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/handler/DefaultCallbackHandler.sol b/protocol/contracts/gnosis-safe-contracts/handler/DefaultCallbackHandler.sol new file mode 100644 index 0000000..71137ca --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/handler/DefaultCallbackHandler.sol @@ -0,0 +1,40 @@ +pragma solidity 0.5.17; + +import "../interfaces/ERC1155TokenReceiver.sol"; +import "../interfaces/ERC721TokenReceiver.sol"; +import "../interfaces/ERC777TokensRecipient.sol"; + +/// @title Default Callback Handler - returns true for known token callbacks +/// @author Richard Meissner - +contract DefaultCallbackHandler is ERC1155TokenReceiver, ERC777TokensRecipient, ERC721TokenReceiver { + + string public constant NAME = "Default Callback Handler"; + string public constant VERSION = "1.0.0"; + + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + external + returns(bytes4) + { + return 0xf23a6e61; + } + + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + returns(bytes4) + { + return 0xbc197c81; + } + + function onERC721Received(address, address, uint256, bytes calldata) + external + returns(bytes4) + { + return 0x150b7a02; + } + + // solium-disable-next-line no-empty-blocks + function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) external { + // We implement this for completeness, doesn't really have any value + } + +} \ No newline at end of file diff --git a/protocol/contracts/gnosis-safe-contracts/interfaces/ERC1155TokenReceiver.sol b/protocol/contracts/gnosis-safe-contracts/interfaces/ERC1155TokenReceiver.sol new file mode 100644 index 0000000..0aadd31 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/interfaces/ERC1155TokenReceiver.sol @@ -0,0 +1,48 @@ +pragma solidity 0.5.17; + +/** + Note: The ERC-165 identifier for this interface is 0x4e2312e0. +*/ +interface ERC1155TokenReceiver { + /** + @notice Handle the receipt of a single ERC1155 token type. + @dev An ERC1155-compliant smart contract MUST call this function on the token recipient contract, at the end of a `safeTransferFrom` after the balance has been updated. + This function MUST return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` (i.e. 0xf23a6e61) if it accepts the transfer. + This function MUST revert if it rejects the transfer. + Return of any other value than the prescribed keccak256 generated value MUST result in the transaction being reverted by the caller. + @param _operator The address which initiated the transfer (i.e. msg.sender) + @param _from The address which previously owned the token + @param _id The ID of the token being transferred + @param _value The amount of tokens being transferred + @param _data Additional data with no specified format + @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + */ + function onERC1155Received( + address _operator, + address _from, + uint256 _id, + uint256 _value, + bytes calldata _data + ) external returns (bytes4); + + /** + @notice Handle the receipt of multiple ERC1155 token types. + @dev An ERC1155-compliant smart contract MUST call this function on the token recipient contract, at the end of a `safeBatchTransferFrom` after the balances have been updated. + This function MUST return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` (i.e. 0xbc197c81) if it accepts the transfer(s). + This function MUST revert if it rejects the transfer(s). + Return of any other value than the prescribed keccak256 generated value MUST result in the transaction being reverted by the caller. + @param _operator The address which initiated the batch transfer (i.e. msg.sender) + @param _from The address which previously owned the token + @param _ids An array containing ids of each token being transferred (order and length must match _values array) + @param _values An array containing amounts of each token being transferred (order and length must match _ids array) + @param _data Additional data with no specified format + @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external returns (bytes4); +} diff --git a/protocol/contracts/gnosis-safe-contracts/interfaces/ERC721TokenReceiver.sol b/protocol/contracts/gnosis-safe-contracts/interfaces/ERC721TokenReceiver.sol new file mode 100644 index 0000000..8bb67a7 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/interfaces/ERC721TokenReceiver.sol @@ -0,0 +1,23 @@ +pragma solidity 0.5.17; + +/// @dev Note: the ERC-165 identifier for this interface is 0x150b7a02. +interface ERC721TokenReceiver { + /// @notice Handle the receipt of an NFT + /// @dev The ERC721 smart contract calls this function on the recipient + /// after a `transfer`. This function MAY throw to revert and reject the + /// transfer. Return of other than the magic value MUST result in the + /// transaction being reverted. + /// Note: the contract address is always the message sender. + /// @param _operator The address which called `safeTransferFrom` function + /// @param _from The address which previously owned the token + /// @param _tokenId The NFT identifier which is being transferred + /// @param _data Additional data with no specified format + /// @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + /// unless throwing + function onERC721Received( + address _operator, + address _from, + uint256 _tokenId, + bytes calldata _data + ) external returns (bytes4); +} diff --git a/protocol/contracts/gnosis-safe-contracts/interfaces/ERC777TokensRecipient.sol b/protocol/contracts/gnosis-safe-contracts/interfaces/ERC777TokensRecipient.sol new file mode 100644 index 0000000..2c57f33 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/interfaces/ERC777TokensRecipient.sol @@ -0,0 +1,12 @@ +pragma solidity 0.5.17; + +interface ERC777TokensRecipient { + function tokensReceived( + address operator, + address from, + address to, + uint256 amount, + bytes calldata data, + bytes calldata operatorData + ) external; +} diff --git a/protocol/contracts/gnosis-safe-contracts/interfaces/ISignatureValidator.sol b/protocol/contracts/gnosis-safe-contracts/interfaces/ISignatureValidator.sol new file mode 100644 index 0000000..eb0212f --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/interfaces/ISignatureValidator.sol @@ -0,0 +1,22 @@ +pragma solidity 0.5.17; + +contract ISignatureValidatorConstants { + // bytes4(keccak256("isValidSignature(bytes,bytes)") + bytes4 internal constant EIP1271_MAGIC_VALUE = 0x20c13b0b; +} + +contract ISignatureValidator is ISignatureValidatorConstants { + /** + * @dev Should return whether the signature provided is valid for the provided data + * @param _data Arbitrary length data signed on the behalf of address(this) + * @param _signature Signature byte array associated with _data + * + * MUST return the bytes4 magic value 0x20c13b0b when function passes. + * MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5) + * MUST allow external calls + */ + function isValidSignature(bytes memory _data, bytes memory _signature) + public + view + returns (bytes4); +} diff --git a/protocol/contracts/gnosis-safe-contracts/libraries/CreateAndAddModules.sol b/protocol/contracts/gnosis-safe-contracts/libraries/CreateAndAddModules.sol new file mode 100644 index 0000000..fbf3d9f --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/libraries/CreateAndAddModules.sol @@ -0,0 +1,60 @@ +pragma solidity ^0.5.0; +import "../base/Module.sol"; + +/// @title Create and Add Modules - Allows to create and add multiple module in one transaction. +/// @author Stefan George - +/// @author Richard Meissner - +contract CreateAndAddModules { + /// @dev Function required to compile contract. Gnosis Safe function is called instead. + /// @param module Not used. + function enableModule(Module module) public { + revert(); + } + + /// @dev Allows to create and add multiple module in one transaction. + /// @param proxyFactory Module proxy factory contract. + /// @param data Modules constructor payload. This is the data for each proxy factory call concatinated. (e.g. ) + function createAndAddModules(address proxyFactory, bytes memory data) + public + { + uint256 length = data.length; + Module module; + uint256 i = 0; + while (i < length) { + // solium-disable-next-line security/no-inline-assembly + assembly { + let createBytesLength := mload(add(0x20, add(data, i))) + let createBytes := add(0x40, add(data, i)) + + let output := mload(0x40) + if eq( + delegatecall( + gas, + proxyFactory, + createBytes, + createBytesLength, + output, + 0x20 + ), + 0 + ) { + revert(0, 0) + } + module := and( + mload(output), + 0xffffffffffffffffffffffffffffffffffffffff + ) + + // Data is always padded to 32 bytes + i := add( + i, + add( + 0x20, + mul(div(add(createBytesLength, 0x1f), 0x20), 0x20) + ) + ) + } + this.enableModule(module); + } + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/libraries/CreateCall.sol b/protocol/contracts/gnosis-safe-contracts/libraries/CreateCall.sol new file mode 100644 index 0000000..1a7dfd7 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/libraries/CreateCall.sol @@ -0,0 +1,41 @@ +pragma solidity 0.5.17; + +/// @title Create Call - Allows to use the different create opcodes to deploy a contract +/// @author Richard Meissner - +contract CreateCall { + event ContractCreation(address newContract); + + function performCreate2( + uint256 value, + bytes memory deploymentData, + bytes32 salt + ) public returns (address newContract) { + // solium-disable-next-line security/no-inline-assembly + assembly { + newContract := create2( + value, + add(0x20, deploymentData), + mload(deploymentData), + salt + ) + } + require(newContract != address(0), "Could not deploy contract"); + emit ContractCreation(newContract); + } + + function performCreate(uint256 value, bytes memory deploymentData) + public + returns (address newContract) + { + // solium-disable-next-line security/no-inline-assembly + assembly { + newContract := create( + value, + add(deploymentData, 0x20), + mload(deploymentData) + ) + } + require(newContract != address(0), "Could not deploy contract"); + emit ContractCreation(newContract); + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/libraries/MultiSend.sol b/protocol/contracts/gnosis-safe-contracts/libraries/MultiSend.sol new file mode 100644 index 0000000..b2717eb --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/libraries/MultiSend.sol @@ -0,0 +1,68 @@ +pragma solidity ^0.5.0; + +/// @title Multi Send - Allows to batch multiple transactions into one. +/// @author Nick Dodson - +/// @author Gonçalo Sá - +/// @author Stefan George - +/// @author Richard Meissner - +contract MultiSend { + bytes32 private constant GUARD_VALUE = keccak256("multisend.guard.bytes32"); + + bytes32 guard; + + constructor() public { + guard = GUARD_VALUE; + } + + /// @dev Sends multiple transactions and reverts all if one fails. + /// @param transactions Encoded transactions. Each transaction is encoded as a packed bytes of + /// operation as a uint8 with 0 for a call or 1 for a delegatecall (=> 1 byte), + /// to as a address (=> 20 bytes), + /// value as a uint256 (=> 32 bytes), + /// data length as a uint256 (=> 32 bytes), + /// data as bytes. + /// see abi.encodePacked for more information on packed encoding + function multiSend(bytes memory transactions) public { + require( + guard != GUARD_VALUE, + "MultiSend should only be called via delegatecall" + ); + // solium-disable-next-line security/no-inline-assembly + assembly { + let length := mload(transactions) + let i := 0x20 + for { + + } lt(i, length) { + + } { + // First byte of the data is the operation. + // We shift by 248 bits (256 - 8 [operation byte]) it right since mload will always load 32 bytes (a word). + // This will also zero out unused data. + let operation := shr(0xf8, mload(add(transactions, i))) + // We offset the load address by 1 byte (operation byte) + // We shift it right by 96 bits (256 - 160 [20 address bytes]) to right-align the data and zero out unused data. + let to := shr(0x60, mload(add(transactions, add(i, 0x01)))) + // We offset the load address by 21 byte (operation byte + 20 address bytes) + let value := mload(add(transactions, add(i, 0x15))) + // We offset the load address by 53 byte (operation byte + 20 address bytes + 32 value bytes) + let dataLength := mload(add(transactions, add(i, 0x35))) + // We offset the load address by 85 byte (operation byte + 20 address bytes + 32 value bytes + 32 data length bytes) + let data := add(transactions, add(i, 0x55)) + let success := 0 + switch operation + case 0 { + success := call(gas, to, value, data, dataLength, 0, 0) + } + case 1 { + success := delegatecall(gas, to, data, dataLength, 0, 0) + } + if eq(success, 0) { + revert(0, 0) + } + // Next entry starts at 85 byte + data length + i := add(i, add(0x55, dataLength)) + } + } + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/modules/DailyLimitModule.sol b/protocol/contracts/gnosis-safe-contracts/modules/DailyLimitModule.sol new file mode 100644 index 0000000..8c91315 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/modules/DailyLimitModule.sol @@ -0,0 +1,112 @@ +pragma solidity 0.5.17; +import "../base/Module.sol"; +import "../base/ModuleManager.sol"; +import "../base/OwnerManager.sol"; +import "../common/Enum.sol"; + +/// @title Daily Limit Module - Allows to transfer limited amounts of ERC20 tokens and Ether without confirmations. +/// @author Stefan George - +contract DailyLimitModule is Module { + string public constant NAME = "Daily Limit Module"; + string public constant VERSION = "0.1.0"; + + // dailyLimits mapping maps token address to daily limit settings. + mapping(address => DailyLimit) public dailyLimits; + + struct DailyLimit { + uint256 dailyLimit; + uint256 spentToday; + uint256 lastDay; + } + + /// @dev Setup function sets initial storage of contract. + /// @param tokens List of token addresses. Ether is represented with address 0x0. + /// @param _dailyLimits List of daily limits in smalles units (e.g. Wei for Ether). + function setup(address[] memory tokens, uint256[] memory _dailyLimits) + public + { + setManager(); + for (uint256 i = 0; i < tokens.length; i++) + dailyLimits[tokens[i]].dailyLimit = _dailyLimits[i]; + } + + /// @dev Allows to update the daily limit for a specified token. This can only be done via a Safe transaction. + /// @param token Token contract address. + /// @param dailyLimit Daily limit in smallest token unit. + function changeDailyLimit(address token, uint256 dailyLimit) + public + authorized + { + dailyLimits[token].dailyLimit = dailyLimit; + } + + /// @dev Returns if Safe transaction is a valid daily limit transaction. + /// @param token Address of the token that should be transfered (0 for Ether) + /// @param to Address to which the tokens should be transfered + /// @param amount Amount of tokens (or Ether) that should be transfered + /// @return Returns if transaction can be executed. + function executeDailyLimit( + address token, + address to, + uint256 amount + ) public { + // Only Safe owners are allowed to execute daily limit transactions. + require( + OwnerManager(address(manager)).isOwner(msg.sender), + "Method can only be called by an owner" + ); + require(to != address(0), "Invalid to address provided"); + require(amount > 0, "Invalid amount provided"); + // Validate that transfer is not exceeding daily limit. + require(isUnderLimit(token, amount), "Daily limit has been reached"); + dailyLimits[token].spentToday += amount; + if (token == address(0)) { + require( + manager.execTransactionFromModule( + to, + amount, + "", + Enum.Operation.Call + ), + "Could not execute ether transfer" + ); + } else { + bytes memory data = abi.encodeWithSignature( + "transfer(address,uint256)", + to, + amount + ); + require( + manager.execTransactionFromModule( + token, + 0, + data, + Enum.Operation.Call + ), + "Could not execute token transfer" + ); + } + } + + function isUnderLimit(address token, uint256 amount) + internal + returns (bool) + { + DailyLimit storage dailyLimit = dailyLimits[token]; + if (today() > dailyLimit.lastDay) { + dailyLimit.lastDay = today(); + dailyLimit.spentToday = 0; + } + if ( + dailyLimit.spentToday + amount <= dailyLimit.dailyLimit && + dailyLimit.spentToday + amount > dailyLimit.spentToday + ) return true; + return false; + } + + /// @dev Returns last midnight as Unix timestamp. + /// @return Unix timestamp. + function today() public view returns (uint256) { + return now - (now % 1 days); + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/modules/SocialRecoveryModule.sol b/protocol/contracts/gnosis-safe-contracts/modules/SocialRecoveryModule.sol new file mode 100644 index 0000000..30a3ecc --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/modules/SocialRecoveryModule.sol @@ -0,0 +1,112 @@ +pragma solidity 0.5.17; +import "../base/Module.sol"; +import "../base/ModuleManager.sol"; +import "../base/OwnerManager.sol"; +import "../common/Enum.sol"; + +/// @title Social Recovery Module - Allows to replace an owner without Safe confirmations if friends approve the replacement. +/// @author Stefan George - +contract SocialRecoveryModule is Module { + string public constant NAME = "Social Recovery Module"; + string public constant VERSION = "0.1.0"; + + uint256 public threshold; + address[] public friends; + + // isFriend mapping maps friend's address to friend status. + mapping(address => bool) public isFriend; + // isExecuted mapping maps data hash to execution status. + mapping(bytes32 => bool) public isExecuted; + // isConfirmed mapping maps data hash to friend's address to confirmation status. + mapping(bytes32 => mapping(address => bool)) public isConfirmed; + + modifier onlyFriend() { + require(isFriend[msg.sender], "Method can only be called by a friend"); + _; + } + + /// @dev Setup function sets initial storage of contract. + /// @param _friends List of friends' addresses. + /// @param _threshold Required number of friends to confirm replacement. + function setup(address[] memory _friends, uint256 _threshold) public { + require( + _threshold <= _friends.length, + "Threshold cannot exceed friends count" + ); + require(_threshold >= 2, "At least 2 friends required"); + setManager(); + // Set allowed friends. + for (uint256 i = 0; i < _friends.length; i++) { + address friend = _friends[i]; + require(friend != address(0), "Invalid friend address provided"); + require(!isFriend[friend], "Duplicate friend address provided"); + isFriend[friend] = true; + } + friends = _friends; + threshold = _threshold; + } + + /// @dev Allows a friend to confirm a Safe transaction. + /// @param dataHash Safe transaction hash. + function confirmTransaction(bytes32 dataHash) public onlyFriend { + require(!isExecuted[dataHash], "Recovery already executed"); + isConfirmed[dataHash][msg.sender] = true; + } + + /// @dev Returns if Safe transaction is a valid owner replacement transaction. + /// @param prevOwner Owner that pointed to the owner to be replaced in the linked list + /// @param oldOwner Owner address to be replaced. + /// @param newOwner New owner address. + /// @return Returns if transaction can be executed. + function recoverAccess( + address prevOwner, + address oldOwner, + address newOwner + ) public onlyFriend { + bytes memory data = abi.encodeWithSignature( + "swapOwner(address,address,address)", + prevOwner, + oldOwner, + newOwner + ); + bytes32 dataHash = getDataHash(data); + require(!isExecuted[dataHash], "Recovery already executed"); + require( + isConfirmedByRequiredFriends(dataHash), + "Recovery has not enough confirmations" + ); + isExecuted[dataHash] = true; + require( + manager.execTransactionFromModule( + address(manager), + 0, + data, + Enum.Operation.Call + ), + "Could not execute recovery" + ); + } + + /// @dev Returns if Safe transaction is a valid owner replacement transaction. + /// @param dataHash Data hash. + /// @return Confirmation status. + function isConfirmedByRequiredFriends(bytes32 dataHash) + public + view + returns (bool) + { + uint256 confirmationCount; + for (uint256 i = 0; i < friends.length; i++) { + if (isConfirmed[dataHash][friends[i]]) confirmationCount++; + if (confirmationCount == threshold) return true; + } + return false; + } + + /// @dev Returns hash of data encoding owner replacement. + /// @param data Data payload. + /// @return Data hash. + function getDataHash(bytes memory data) public pure returns (bytes32) { + return keccak256(data); + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/modules/StateChannelModule.sol b/protocol/contracts/gnosis-safe-contracts/modules/StateChannelModule.sol new file mode 100644 index 0000000..ec0867a --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/modules/StateChannelModule.sol @@ -0,0 +1,109 @@ +pragma solidity 0.5.17; +import "../base/Module.sol"; +import "../base/OwnerManager.sol"; +import "../common/Enum.sol"; +import "../common/SignatureDecoder.sol"; + +/// @title Gnosis Safe State Module - A module that allows interaction with statechannels. +/// @author Stefan George - +/// @author Richard Meissner - +contract StateChannelModule is Module, SignatureDecoder { + string public constant NAME = "State Channel Module"; + string public constant VERSION = "0.1.0"; + + // isExecuted mapping allows to check if a transaction (by hash) was already executed. + mapping(bytes32 => uint256) public isExecuted; + + /// @dev Setup function sets manager + function setup() public { + setManager(); + } + + /// @dev Allows to execute a Safe transaction confirmed by required number of owners. + /// @param to Destination address of Safe transaction. + /// @param value Ether value of Safe transaction. + /// @param data Data payload of Safe transaction. + /// @param operation Operation type of Safe transaction. + /// @param nonce Nonce used for this Safe transaction. + /// @param signatures Packed signature data ({bytes32 r}{bytes32 s}{uint8 v}) + function execTransaction( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation, + uint256 nonce, + bytes memory signatures + ) public { + bytes32 transactionHash = getTransactionHash( + to, + value, + data, + operation, + nonce + ); + require( + isExecuted[transactionHash] == 0, + "Transaction already executed" + ); + checkHash(transactionHash, signatures); + // Mark as executed and execute transaction. + isExecuted[transactionHash] = 1; + require( + manager.execTransactionFromModule(to, value, data, operation), + "Could not execute transaction" + ); + } + + function checkHash(bytes32 transactionHash, bytes memory signatures) + internal + view + { + // There cannot be an owner with address 0. + address lastOwner = address(0); + address currentOwner; + uint256 i; + uint256 threshold = OwnerManager(address(manager)).getThreshold(); + // Validate threshold is reached. + for (i = 0; i < threshold; i++) { + currentOwner = recoverKey(transactionHash, signatures, i); + require( + OwnerManager(address(manager)).isOwner(currentOwner), + "Signature not provided by owner" + ); + require( + currentOwner > lastOwner, + "Signatures are not ordered by owner address" + ); + lastOwner = currentOwner; + } + } + + /// @dev Returns hash to be signed by owners. + /// @param to Destination address. + /// @param value Ether value. + /// @param data Data payload. + /// @param operation Operation type. + /// @param nonce Transaction nonce. + /// @return Transaction hash. + function getTransactionHash( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation, + uint256 nonce + ) public view returns (bytes32) { + return + keccak256( + abi.encodePacked( + bytes1(0x19), + bytes1(0), + this, + to, + value, + data, + operation, + nonce + ) + ); + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/modules/WhitelistModule.sol b/protocol/contracts/gnosis-safe-contracts/modules/WhitelistModule.sol new file mode 100644 index 0000000..8cf3cd1 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/modules/WhitelistModule.sol @@ -0,0 +1,68 @@ +pragma solidity 0.5.17; +import "../base/Module.sol"; +import "../base/ModuleManager.sol"; +import "../base/OwnerManager.sol"; +import "../common/Enum.sol"; + +/// @title Whitelist Module - Allows to execute transactions to whitelisted addresses without confirmations. +/// @author Stefan George - +contract WhitelistModule is Module { + string public constant NAME = "Whitelist Module"; + string public constant VERSION = "0.1.0"; + + // isWhitelisted mapping maps destination address to boolean. + mapping(address => bool) public isWhitelisted; + + /// @dev Setup function sets initial storage of contract. + /// @param accounts List of whitelisted accounts. + function setup(address[] memory accounts) public { + setManager(); + for (uint256 i = 0; i < accounts.length; i++) { + address account = accounts[i]; + require(account != address(0), "Invalid account provided"); + isWhitelisted[account] = true; + } + } + + /// @dev Allows to add destination to whitelist. This can only be done via a Safe transaction. + /// @param account Destination address. + function addToWhitelist(address account) public authorized { + require(account != address(0), "Invalid account provided"); + require(!isWhitelisted[account], "Account is already whitelisted"); + isWhitelisted[account] = true; + } + + /// @dev Allows to remove destination from whitelist. This can only be done via a Safe transaction. + /// @param account Destination address. + function removeFromWhitelist(address account) public authorized { + require(isWhitelisted[account], "Account is not whitelisted"); + isWhitelisted[account] = false; + } + + /// @dev Returns if Safe transaction is to a whitelisted destination. + /// @param to Whitelisted destination address. + /// @param value Not checked. + /// @param data Not checked. + /// @return Returns if transaction can be executed. + function executeWhitelisted( + address to, + uint256 value, + bytes memory data + ) public returns (bool) { + // Only Safe owners are allowed to execute transactions to whitelisted accounts. + require( + OwnerManager(address(manager)).isOwner(msg.sender), + "Method can only be called by an owner" + ); + require(isWhitelisted[to], "Target account is not whitelisted"); + require( + manager.execTransactionFromModule( + to, + value, + data, + Enum.Operation.Call + ), + "Could not execute transaction" + ); + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/proxies/DelegateConstructorProxy.sol b/protocol/contracts/gnosis-safe-contracts/proxies/DelegateConstructorProxy.sol new file mode 100644 index 0000000..7577636 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/proxies/DelegateConstructorProxy.sol @@ -0,0 +1,38 @@ +pragma solidity 0.5.17; +import "./GnosisSafeProxy.sol"; + +/// @title Delegate Constructor Proxy - Generic proxy contract allows to execute all transactions applying the code of a master contract. It is possible to send along initialization data with the constructor. +/// @author Stefan George - +/// @author Richard Meissner - +contract DelegateConstructorProxy is GnosisSafeProxy { + /// @dev Constructor function sets address of master copy contract. + /// @param _masterCopy Master copy address. + /// @param initializer Data used for a delegate call to initialize the contract. + constructor(address _masterCopy, bytes memory initializer) + public + GnosisSafeProxy(_masterCopy) + { + if (initializer.length > 0) { + // solium-disable-next-line security/no-inline-assembly + assembly { + let masterCopy := and( + sload(0), + 0xffffffffffffffffffffffffffffffffffffffff + ) + let success := delegatecall( + sub(gas, 10000), + masterCopy, + add(initializer, 0x20), + mload(initializer), + 0, + 0 + ) + let ptr := mload(0x40) + returndatacopy(ptr, 0, returndatasize()) + if eq(success, 0) { + revert(ptr, returndatasize()) + } + } + } + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/proxies/GnosisSafeProxy.sol b/protocol/contracts/gnosis-safe-contracts/proxies/GnosisSafeProxy.sol new file mode 100644 index 0000000..ce504f8 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/proxies/GnosisSafeProxy.sol @@ -0,0 +1,59 @@ +pragma solidity ^0.5.0; + +/// @title IProxy - Helper interface to access masterCopy of the Proxy on-chain +/// @author Richard Meissner - +interface IProxy { + function masterCopy() external view returns (address); +} + +/// @title GnosisSafeProxy - Generic proxy contract allows to execute all transactions applying the code of a master contract. +/// @author Stefan George - +/// @author Richard Meissner - +contract GnosisSafeProxy { + // masterCopy always needs to be first declared variable, to ensure that it is at the same location in the contracts to which calls are delegated. + // To reduce deployment costs this variable is internal and needs to be retrieved via `getStorageAt` + address internal masterCopy; + + /// @dev Constructor function sets address of master copy contract. + /// @param _masterCopy Master copy address. + constructor(address _masterCopy) public { + require( + _masterCopy != address(0), + "Invalid master copy address provided" + ); + masterCopy = _masterCopy; + } + + /// @dev Fallback function forwards all transactions and returns all received return data. + function() external payable { + // solium-disable-next-line security/no-inline-assembly + assembly { + let masterCopy := and( + sload(0), + 0xffffffffffffffffffffffffffffffffffffffff + ) + // 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s + if eq( + calldataload(0), + 0xa619486e00000000000000000000000000000000000000000000000000000000 + ) { + mstore(0, masterCopy) + return(0, 0x20) + } + calldatacopy(0, 0, calldatasize()) + let success := delegatecall( + gas, + masterCopy, + 0, + calldatasize(), + 0, + 0 + ) + returndatacopy(0, 0, returndatasize()) + if eq(success, 0) { + revert(0, returndatasize()) + } + return(0, returndatasize()) + } + } +} diff --git a/protocol/contracts/gnosis-safe-contracts/proxies/GnosisSafeProxyFactory.sol b/protocol/contracts/gnosis-safe-contracts/proxies/GnosisSafeProxyFactory.sol new file mode 100644 index 0000000..34e6e1c --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/proxies/GnosisSafeProxyFactory.sol @@ -0,0 +1,139 @@ +pragma solidity ^0.5.3; +import "./GnosisSafeProxy.sol"; +import "./IProxyCreationCallback.sol"; + +/// @title Proxy Factory - Allows to create new proxy contact and execute a message call to the new proxy within one transaction. +/// @author Stefan George - +contract GnosisSafeProxyFactory { + event ProxyCreation(GnosisSafeProxy proxy); + + /// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction. + /// @param masterCopy Address of master copy. + /// @param data Payload for message call sent to new proxy contract. + function createProxy(address masterCopy, bytes memory data) + public + returns (GnosisSafeProxy proxy) + { + proxy = new GnosisSafeProxy(masterCopy); + if (data.length > 0) + // solium-disable-next-line security/no-inline-assembly + assembly { + if eq( + call(gas, proxy, 0, add(data, 0x20), mload(data), 0, 0), + 0 + ) { + revert(0, 0) + } + } + emit ProxyCreation(proxy); + } + + /// @dev Allows to retrieve the runtime code of a deployed Proxy. This can be used to check that the expected Proxy was deployed. + function proxyRuntimeCode() public pure returns (bytes memory) { + return type(GnosisSafeProxy).runtimeCode; + } + + /// @dev Allows to retrieve the creation code used for the Proxy deployment. With this it is easily possible to calculate predicted address. + function proxyCreationCode() public pure returns (bytes memory) { + return type(GnosisSafeProxy).creationCode; + } + + /// @dev Allows to create new proxy contact using CREATE2 but it doesn't run the initializer. + /// This method is only meant as an utility to be called from other methods + /// @param _mastercopy Address of master copy. + /// @param initializer Payload for message call sent to new proxy contract. + /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. + function deployProxyWithNonce( + address _mastercopy, + bytes memory initializer, + uint256 saltNonce + ) internal returns (GnosisSafeProxy proxy) { + // If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating it + bytes32 salt = keccak256( + abi.encodePacked(keccak256(initializer), saltNonce) + ); + bytes memory deploymentData = abi.encodePacked( + type(GnosisSafeProxy).creationCode, + uint256(_mastercopy) + ); + // solium-disable-next-line security/no-inline-assembly + assembly { + proxy := create2( + 0x0, + add(0x20, deploymentData), + mload(deploymentData), + salt + ) + } + require(address(proxy) != address(0), "Create2 call failed"); + } + + /// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction. + /// @param _mastercopy Address of master copy. + /// @param initializer Payload for message call sent to new proxy contract. + /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. + function createProxyWithNonce( + address _mastercopy, + bytes memory initializer, + uint256 saltNonce + ) public returns (GnosisSafeProxy proxy) { + proxy = deployProxyWithNonce(_mastercopy, initializer, saltNonce); + if (initializer.length > 0) + // solium-disable-next-line security/no-inline-assembly + assembly { + if eq( + call( + gas, + proxy, + 0, + add(initializer, 0x20), + mload(initializer), + 0, + 0 + ), + 0 + ) { + revert(0, 0) + } + } + emit ProxyCreation(proxy); + } + + /// @dev Allows to create new proxy contact, execute a message call to the new proxy and call a specified callback within one transaction + /// @param _mastercopy Address of master copy. + /// @param initializer Payload for message call sent to new proxy contract. + /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. + /// @param callback Callback that will be invoced after the new proxy contract has been successfully deployed and initialized. + function createProxyWithCallback( + address _mastercopy, + bytes memory initializer, + uint256 saltNonce, + IProxyCreationCallback callback + ) public returns (GnosisSafeProxy proxy) { + uint256 saltNonceWithCallback = uint256( + keccak256(abi.encodePacked(saltNonce, callback)) + ); + proxy = createProxyWithNonce( + _mastercopy, + initializer, + saltNonceWithCallback + ); + if (address(callback) != address(0)) + callback.proxyCreated(proxy, _mastercopy, initializer, saltNonce); + } + + /// @dev Allows to get the address for a new proxy contact created via `createProxyWithNonce` + /// This method is only meant for address calculation purpose when you use an initializer that would revert, + /// therefore the response is returned with a revert. When calling this method set `from` to the address of the proxy factory. + /// @param _mastercopy Address of master copy. + /// @param initializer Payload for message call sent to new proxy contract. + /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. + function calculateCreateProxyWithNonceAddress( + address _mastercopy, + bytes calldata initializer, + uint256 saltNonce + ) external returns (GnosisSafeProxy proxy) { + proxy = deployProxyWithNonce(_mastercopy, initializer, saltNonce); + revert(string(abi.encodePacked(proxy))); + } +} \ No newline at end of file diff --git a/protocol/contracts/gnosis-safe-contracts/proxies/IProxyCreationCallback.sol b/protocol/contracts/gnosis-safe-contracts/proxies/IProxyCreationCallback.sol new file mode 100644 index 0000000..15d9a54 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/proxies/IProxyCreationCallback.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.3; +import "./GnosisSafeProxy.sol"; + +interface IProxyCreationCallback { + function proxyCreated(GnosisSafeProxy proxy, address _mastercopy, bytes calldata initializer, uint256 saltNonce) external; +} diff --git a/protocol/contracts/gnosis-safe-contracts/proxies/PayingProxy.sol b/protocol/contracts/gnosis-safe-contracts/proxies/PayingProxy.sol new file mode 100644 index 0000000..a9a8dc8 --- /dev/null +++ b/protocol/contracts/gnosis-safe-contracts/proxies/PayingProxy.sol @@ -0,0 +1,37 @@ +pragma solidity 0.5.17; +import "../common/SecuredTokenTransfer.sol"; +import "./DelegateConstructorProxy.sol"; + +/// @title Paying Proxy - Generic proxy contract allows to execute all transactions applying the code of a master contract. It is possible to send along initialization data with the constructor. And sends funds after creation to a specified account. +/// @author Stefan George - +/// @author Richard Meissner - +contract PayingProxy is DelegateConstructorProxy, SecuredTokenTransfer { + /// @dev Constructor function sets address of master copy contract. + /// @param _masterCopy Master copy address. + /// @param initializer Data used for a delegate call to initialize the contract. + /// @param funder Address that should be paid for the execution of this call + /// @param paymentToken Token that should be used for the payment (0 is ETH) + /// @param payment Value that should be paid + constructor( + address _masterCopy, + bytes memory initializer, + address payable funder, + address paymentToken, + uint256 payment + ) public DelegateConstructorProxy(_masterCopy, initializer) { + if (payment > 0) { + if (paymentToken == address(0)) { + // solium-disable-next-line security/no-send + require( + funder.send(payment), + "Could not pay safe creation with ether" + ); + } else { + require( + transferToken(paymentToken, funder, payment), + "Could not pay safe creation with token" + ); + } + } + } +} diff --git a/protocol/contracts/governance/GovernanceStaking.sol b/protocol/contracts/governance/GovernanceStaking.sol new file mode 100644 index 0000000..d266fe2 --- /dev/null +++ b/protocol/contracts/governance/GovernanceStaking.sol @@ -0,0 +1,506 @@ +/** + *Submitted for verification at Etherscan.io on 2020-07-29 + */ + +/* + ____ __ __ __ _ + / __/__ __ ___ / /_ / / ___ / /_ (_)__ __ + _\ \ / // // _ \/ __// _ \/ -_)/ __// / \ \ / +/___/ \_, //_//_/\__//_//_/\__/ \__//_/ /_\_\ + /___/ + +* Synthetix: YFIRewards.sol +* +* Docs: https://docs.synthetix.io/ +* +* +* MIT License +* =========== +* +* Copyright (c) 2020 Synthetix +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +*/ + +// File: contracts/IRewardDistributionRecipient.sol + +pragma solidity ^0.5.0; + +import "@openzeppelinV2/contracts/math/Math.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/GSN/Context.sol"; +import "@openzeppelinV2/contracts/ownership/Ownable.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +contract IRewardDistributionRecipient is Ownable { + address rewardDistribution; + + function notifyRewardAmount(uint256 reward) external; + + modifier onlyRewardDistribution() { + require( + _msgSender() == rewardDistribution, + "Caller is not reward distribution" + ); + _; + } + + function setRewardDistribution(address _rewardDistribution) + external + onlyOwner + { + rewardDistribution = _rewardDistribution; + } +} + +// File: contracts/CurveRewards.sol + +pragma solidity ^0.5.0; + +contract LPTokenWrapper { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + IERC20 public vote; //sdt token + + uint256 private _totalSupply; + mapping(address => uint256) private _balances; + + function setVote(address _vote) public { + vote = IERC20(_vote); + } + + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + function stake(uint256 amount) public { + _totalSupply = _totalSupply.add(amount); + _balances[msg.sender] = _balances[msg.sender].add(amount); + vote.safeTransferFrom(msg.sender, address(this), amount); + } + + function withdraw(uint256 amount) public { + _totalSupply = _totalSupply.sub(amount); + _balances[msg.sender] = _balances[msg.sender].sub(amount); + vote.safeTransfer(msg.sender, amount); + } +} + +interface Executor { + function execute( + uint256, + uint256, + uint256, + uint256 + ) external; +} + +contract StakeDaoGovernance is LPTokenWrapper, IRewardDistributionRecipient { + /* Fee collection for any other token */ + + function seize(IERC20 _token, uint256 amount) external { + require(msg.sender == governance, "!governance"); + require(_token != token, "reward"); + require(_token != vote, "vote"); + _token.safeTransfer(governance, amount); + } + + /* Fees breaker, to protect withdraws if anything ever goes wrong */ + + bool public breaker = false; + + function setBreaker(bool _breaker) external { + require(msg.sender == governance, "!governance"); + breaker = _breaker; + } + + /* Modifications for proposals */ + + mapping(address => uint256) public voteLock; // period that your sake it locked to keep it for voting + + struct Proposal { + uint256 id; + address proposer; + mapping(address => uint256) forVotes; + mapping(address => uint256) againstVotes; + uint256 totalForVotes; + uint256 totalAgainstVotes; + uint256 start; // block start; + uint256 end; // start + period + address executor; + string hash; + uint256 totalVotesAvailable; + uint256 quorum; + uint256 quorumRequired; + bool open; + } + + mapping(uint256 => Proposal) public proposals; + uint256 public proposalCount; + uint256 public period = 17280; // voting period in blocks ~ 17280 3 days for 15s/block + uint256 public lock = 17280; // vote lock in blocks ~ 17280 3 days for 15s/block + uint256 public minimum = 1e18; + uint256 public quorum = 2000; + bool public config = true; + + address public governance; + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setQuorum(uint256 _quorum) public { + require(msg.sender == governance, "!governance"); + quorum = _quorum; + } + + function setMinimum(uint256 _minimum) public { + require(msg.sender == governance, "!governance"); + minimum = _minimum; + } + + function setPeriod(uint256 _period) public { + require(msg.sender == governance, "!governance"); + period = _period; + } + + function setLock(uint256 _lock) public { + require(msg.sender == governance, "!governance"); + lock = _lock; + } + + function setToken(address _token) public { + require(msg.sender == governance, "!governance"); + token = IERC20(_token); + } + + function initialize( + uint256 id, + address _governance, + address _token, + address _vote + ) public { + require(config == true, "!config"); + setVote(_vote); + config = false; + proposalCount = id; + governance = _governance; //govGSP + token = IERC20(_token); + } + + event NewProposal( + uint256 id, + address creator, + uint256 start, + uint256 duration, + address executor + ); + event Vote( + uint256 indexed id, + address indexed voter, + bool vote, + uint256 weight + ); + + function propose(address executor, string memory hash) public { + require(votesOf(msg.sender) > minimum, "= proposals[id].quorumRequired) { + _quorum = true; + } + proposals[id].open = false; + emit ProposalFinished(id, _for, _against, _quorum); + } + + function votesOf(address voter) public view returns (uint256) { + return votes[voter]; + } + + uint256 public totalVotes; + mapping(address => uint256) public votes; + event RegisterVoter(address voter, uint256 votes, uint256 totalVotes); + event RevokeVoter(address voter, uint256 votes, uint256 totalVotes); + + function register() public { + require(voters[msg.sender] == false, "voter"); + voters[msg.sender] = true; + votes[msg.sender] = balanceOf(msg.sender); + totalVotes = totalVotes.add(votes[msg.sender]); + emit RegisterVoter(msg.sender, votes[msg.sender], totalVotes); + } + + function revoke() public { + require(voters[msg.sender] == true, "!voter"); + voters[msg.sender] = false; + if (totalVotes < votes[msg.sender]) { + //edge case, should be impossible, but this is defi + totalVotes = 0; + } else { + totalVotes = totalVotes.sub(votes[msg.sender]); + } + emit RevokeVoter(msg.sender, votes[msg.sender], totalVotes); + votes[msg.sender] = 0; + } + + mapping(address => bool) public voters; + + function voteFor(uint256 id) public { + require(proposals[id].start < block.number, " block.number, ">end"); + + uint256 _against = proposals[id].againstVotes[msg.sender]; + if (_against > 0) { + proposals[id].totalAgainstVotes = proposals[id] + .totalAgainstVotes + .sub(_against); + proposals[id].againstVotes[msg.sender] = 0; + } + + uint256 vote = votesOf(msg.sender).sub( + proposals[id].forVotes[msg.sender] + ); + proposals[id].totalForVotes = proposals[id].totalForVotes.add(vote); + proposals[id].forVotes[msg.sender] = votesOf(msg.sender); + + proposals[id].totalVotesAvailable = totalVotes; + uint256 _votes = proposals[id].totalForVotes.add( + proposals[id].totalAgainstVotes + ); + proposals[id].quorum = _votes.mul(10000).div(totalVotes); + + voteLock[msg.sender] = lock.add(block.number); + + emit Vote(id, msg.sender, true, vote); + } + + function voteAgainst(uint256 id) public { + require(proposals[id].start < block.number, " block.number, ">end"); + + uint256 _for = proposals[id].forVotes[msg.sender]; + if (_for > 0) { + proposals[id].totalForVotes = proposals[id].totalForVotes.sub(_for); + proposals[id].forVotes[msg.sender] = 0; + } + + uint256 vote = votesOf(msg.sender).sub( + proposals[id].againstVotes[msg.sender] + ); + proposals[id].totalAgainstVotes = proposals[id].totalAgainstVotes.add( + vote + ); + proposals[id].againstVotes[msg.sender] = votesOf(msg.sender); + + proposals[id].totalVotesAvailable = totalVotes; + uint256 _votes = proposals[id].totalForVotes.add( + proposals[id].totalAgainstVotes + ); + proposals[id].quorum = _votes.mul(10000).div(totalVotes); + + voteLock[msg.sender] = lock.add(block.number); + + emit Vote(id, msg.sender, false, vote); + } + + /* Default rewards contract */ + + IERC20 public token; //yCRV token + + uint256 public constant DURATION = 7 days; + + uint256 public periodFinish = 0; + uint256 public rewardRate = 0; + uint256 public lastUpdateTime; + uint256 public rewardPerTokenStored; + mapping(address => uint256) public userRewardPerTokenPaid; + mapping(address => uint256) public rewards; + + event RewardAdded(uint256 reward); + event Staked(address indexed user, uint256 amount); + event Withdrawn(address indexed user, uint256 amount); + event RewardPaid(address indexed user, uint256 reward); + + modifier updateReward(address account) { + rewardPerTokenStored = rewardPerToken(); + lastUpdateTime = lastTimeRewardApplicable(); + if (account != address(0)) { + rewards[account] = earned(account); + userRewardPerTokenPaid[account] = rewardPerTokenStored; + } + _; + } + + function lastTimeRewardApplicable() public view returns (uint256) { + return Math.min(block.timestamp, periodFinish); + } + + function rewardPerToken() public view returns (uint256) { + if (totalSupply() == 0) { + return rewardPerTokenStored; + } + return + rewardPerTokenStored.add( + lastTimeRewardApplicable() + .sub(lastUpdateTime) + .mul(rewardRate) + .mul(1e18) + .div(totalSupply()) + ); + } + + function earned(address account) public view returns (uint256) { + return + balanceOf(account) + .mul(rewardPerToken().sub(userRewardPerTokenPaid[account])) + .div(1e18) + .add(rewards[account]); + } + + // stake visibility is public as overriding LPTokenWrapper's stake() function + function stake(uint256 amount) public updateReward(msg.sender) { + require(amount > 0, "Cannot stake 0"); + if (voters[msg.sender] == true) { + votes[msg.sender] = votes[msg.sender].add(amount); + totalVotes = totalVotes.add(amount); + } + super.stake(amount); + emit Staked(msg.sender, amount); + } + + function withdraw(uint256 amount) public updateReward(msg.sender) { + require(amount > 0, "Cannot withdraw 0"); + if (voters[msg.sender] == true) { + votes[msg.sender] = votes[msg.sender].sub(amount); + totalVotes = totalVotes.sub(amount); + } + if (breaker == false) { + require(voteLock[msg.sender] < block.number, "!locked"); + } + super.withdraw(amount); + emit Withdrawn(msg.sender, amount); + } + + function exit() external { + withdraw(balanceOf(msg.sender)); + getReward(); + } + + function getReward() public updateReward(msg.sender) { + if (breaker == false) { + require(voteLock[msg.sender] > block.number, "!voted"); + } + uint256 reward = earned(msg.sender); + if (reward > 0) { + rewards[msg.sender] = 0; + token.safeTransfer(msg.sender, reward); + emit RewardPaid(msg.sender, reward); + } + } + + function notifyRewardAmount(uint256 reward) + external + onlyRewardDistribution + updateReward(address(0)) + { + IERC20(token).safeTransferFrom(msg.sender, address(this), reward); + if (block.timestamp >= periodFinish) { + rewardRate = reward.div(DURATION); + } else { + uint256 remaining = periodFinish.sub(block.timestamp); + uint256 leftover = remaining.mul(rewardRate); + rewardRate = reward.add(leftover).div(DURATION); + } + lastUpdateTime = block.timestamp; + periodFinish = block.timestamp.add(DURATION); + emit RewardAdded(reward); + } +} diff --git a/protocol/contracts/governance/MasterChef.sol b/protocol/contracts/governance/MasterChef.sol new file mode 100644 index 0000000..251786d --- /dev/null +++ b/protocol/contracts/governance/MasterChef.sol @@ -0,0 +1,320 @@ +/** + *Submitted for verification at Etherscan.io on 2020-09-09 + */ + +// SPDX-License-Identifier: MIT +// File: @openzeppelin/contracts/token/ERC20/IERC20.sol + +pragma solidity ^0.6.0; + +import "../temp/openzeppelin/IERC20.sol"; +import "../temp/openzeppelin/SafeMath.sol"; +import "../temp/openzeppelin/Address.sol"; +import "../temp/openzeppelin/SafeERC20.sol"; +import "../temp/openzeppelin/EnumerableSet.sol"; +import "../temp/openzeppelin/Context.sol"; +import "../temp/openzeppelin/Ownable.sol"; +import "../temp/openzeppelin/ERC20.sol"; + +// File: contracts/StakeDaoToken.sol + +pragma solidity 0.6.12; + +// StakeDaoToken with Governance. +contract StakeDaoToken is ERC20("Stake Dao Token", "SDT"), Ownable { + /// @notice Creates `_amount` token to `_to`. Must only be called by the owner (MasterChef). + function mint(address _to, uint256 _amount) public onlyOwner { + _mint(_to, _amount); + } +} + +// File: contracts/curve/ICurveFiCurve.sol + +pragma solidity ^0.6.12; + +interface ICurveFiCurve { + // All we care about is the ratio of each coin + function balances(int128 arg0) external returns (uint256 out); +} + +// File: contracts/MasterChef.sol + +pragma solidity 0.6.12; + +// MasterChef was the master of sdt. He now governs over SDT. He can make SDTs and he is a fair guy. +// +// Note that it's ownable and the owner wields tremendous power. The ownership +// will be transferred to a governance smart contract once SDTS is sufficiently +// distributed and the community can show to govern itself. +// +// Have fun reading it. Hopefully it's bug-free. God bless. +contract MasterChef is Ownable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + // Info of each user. + struct UserInfo { + uint256 amount; // How many LP tokens the user has provided. + uint256 rewardDebt; // Reward debt. See explanation below. + // + // We do some fancy math here. Basically, any point in time, the amount of SDTs + // entitled to a user but is pending to be distributed is: + // + // pending reward = (user.amount * pool.accSdtPerShare) - user.rewardDebt + // + // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens: + // 1. The pool's `accSdtPerShare` (and `lastRewardBlock`) gets updated. + // 2. User receives the pending reward sent to his/her address. + // 3. User's `amount` gets updated. + // 4. User's `rewardDebt` gets updated. + } + + // Info of each pool. + struct PoolInfo { + IERC20 lpToken; // Address of LP token contract. + uint256 allocPoint; // How many allocation points assigned to this pool. SDTs to distribute per block. + uint256 lastRewardBlock; // Last block number that SDTs distribution occurs. + uint256 accSdtPerShare; // Accumulated SDTs per share, times 1e12. See below. + } + + // The SDT TOKEN! + StakeDaoToken public sdt; + // Dev fund (2%, initially) + uint256 public devFundDivRate = 50; + // Dev address. + address public devaddr; + // Block number when bonus SDT period ends. + uint256 public bonusEndBlock; + // SDT tokens created per block. + uint256 public sdtPerBlock; + // Bonus muliplier for early sdt makers. + uint256 public constant BONUS_MULTIPLIER = 2; + + // Info of each pool. + PoolInfo[] public poolInfo; + // Info of each user that stakes LP tokens. + mapping(uint256 => mapping(address => UserInfo)) public userInfo; + // Total allocation points. Must be the sum of all allocation points in all pools. + uint256 public totalAllocPoint = 0; + // The block number when SDT mining starts. + uint256 public startBlock; + + // Events + event Recovered(address token, uint256 amount); + event Deposit(address indexed user, uint256 indexed pid, uint256 amount); + event Withdraw(address indexed user, uint256 indexed pid, uint256 amount); + event EmergencyWithdraw( + address indexed user, + uint256 indexed pid, + uint256 amount + ); + + constructor( + StakeDaoToken _sdt, + address _devaddr, + uint256 _sdtPerBlock, + uint256 _startBlock, + uint256 _bonusEndBlock + ) public { + sdt = _sdt; + devaddr = _devaddr; + sdtPerBlock = _sdtPerBlock; + bonusEndBlock = _bonusEndBlock; + startBlock = _startBlock; + } + + function poolLength() external view returns (uint256) { + return poolInfo.length; + } + + // Add a new lp to the pool. Can only be called by the owner. + // XXX DO NOT add the same LP token more than once. Rewards will be messed up if you do. + function add( + uint256 _allocPoint, + IERC20 _lpToken, + bool _withUpdate + ) public onlyOwner { + if (_withUpdate) { + massUpdatePools(); + } + uint256 lastRewardBlock = + block.number > startBlock ? block.number : startBlock; + totalAllocPoint = totalAllocPoint.add(_allocPoint); + poolInfo.push( + PoolInfo({ + lpToken: _lpToken, + allocPoint: _allocPoint, + lastRewardBlock: lastRewardBlock, + accSdtPerShare: 0 + }) + ); + } + + // Update the given pool's SDT allocation point. Can only be called by the owner. + function set( + uint256 _pid, + uint256 _allocPoint, + bool _withUpdate + ) public onlyOwner { + if (_withUpdate) { + massUpdatePools(); + } + totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add( + _allocPoint + ); + poolInfo[_pid].allocPoint = _allocPoint; + } + + // Return reward multiplier over the given _from to _to block. + function getMultiplier(uint256 _from, uint256 _to) + public + view + returns (uint256) + { + if (_to <= bonusEndBlock) { + return _to.sub(_from).mul(BONUS_MULTIPLIER); + } else if (_from >= bonusEndBlock) { + return _to.sub(_from); + } else { + return + bonusEndBlock.sub(_from).mul(BONUS_MULTIPLIER).add( + _to.sub(bonusEndBlock) + ); + } + } + + // View function to see pending SDTs on frontend. + function pendingSdt(uint256 _pid, address _user) + external + view + returns (uint256) + { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][_user]; + uint256 accSdtPerShare = pool.accSdtPerShare; + uint256 lpSupply = pool.lpToken.balanceOf(address(this)); + if (block.number > pool.lastRewardBlock && lpSupply != 0) { + uint256 multiplier = + getMultiplier(pool.lastRewardBlock, block.number); + uint256 sdtReward = + multiplier.mul(sdtPerBlock).mul(pool.allocPoint).div( + totalAllocPoint + ); + accSdtPerShare = accSdtPerShare.add( + sdtReward.mul(1e12).div(lpSupply) + ); + } + return user.amount.mul(accSdtPerShare).div(1e12).sub(user.rewardDebt); + } + + // Update reward vairables for all pools. Be careful of gas spending! + function massUpdatePools() public { + uint256 length = poolInfo.length; + for (uint256 pid = 0; pid < length; ++pid) { + updatePool(pid); + } + } + + // Update reward variables of the given pool to be up-to-date. + function updatePool(uint256 _pid) public { + PoolInfo storage pool = poolInfo[_pid]; + if (block.number <= pool.lastRewardBlock) { + return; + } + uint256 lpSupply = pool.lpToken.balanceOf(address(this)); + if (lpSupply == 0) { + pool.lastRewardBlock = block.number; + return; + } + uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number); + uint256 sdtReward = + multiplier.mul(sdtPerBlock).mul(pool.allocPoint).div( + totalAllocPoint + ); + // sdt.transfer(devaddr, sdtReward.div(devFundDivRate)); + sdt.transferFrom(devaddr, address(this), sdtReward); + pool.accSdtPerShare = pool.accSdtPerShare.add( + sdtReward.mul(1e12).div(lpSupply) + ); + pool.lastRewardBlock = block.number; + } + + // Deposit LP tokens to MasterChef for SDT allocation. + function deposit(uint256 _pid, uint256 _amount) public { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][msg.sender]; + updatePool(_pid); + if (user.amount > 0) { + uint256 pending = + user.amount.mul(pool.accSdtPerShare).div(1e12).sub( + user.rewardDebt + ); + safeSdtTransfer(msg.sender, pending); + } + pool.lpToken.safeTransferFrom( + address(msg.sender), + address(this), + _amount + ); + user.amount = user.amount.add(_amount); + user.rewardDebt = user.amount.mul(pool.accSdtPerShare).div(1e12); + emit Deposit(msg.sender, _pid, _amount); + } + + // Withdraw LP tokens from MasterChef. + function withdraw(uint256 _pid, uint256 _amount) public { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][msg.sender]; + require(user.amount >= _amount, "withdraw: not good"); + updatePool(_pid); + uint256 pending = + user.amount.mul(pool.accSdtPerShare).div(1e12).sub(user.rewardDebt); + safeSdtTransfer(msg.sender, pending); + user.amount = user.amount.sub(_amount); + user.rewardDebt = user.amount.mul(pool.accSdtPerShare).div(1e12); + pool.lpToken.safeTransfer(address(msg.sender), _amount); + emit Withdraw(msg.sender, _pid, _amount); + } + + // Withdraw without caring about rewards. EMERGENCY ONLY. + function emergencyWithdraw(uint256 _pid) public { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][msg.sender]; + pool.lpToken.safeTransfer(address(msg.sender), user.amount); + emit EmergencyWithdraw(msg.sender, _pid, user.amount); + user.amount = 0; + user.rewardDebt = 0; + } + + // Safe sdt transfer function, just in case if rounding error causes pool to not have enough SDTs. + function safeSdtTransfer(address _to, uint256 _amount) internal { + uint256 sdtBal = sdt.balanceOf(address(this)); + if (_amount > sdtBal) { + sdt.transfer(_to, sdtBal); + } else { + sdt.transfer(_to, _amount); + } + } + + // Update dev address by the owner. + function setDevAddress(address _devaddr) public onlyOwner { + devaddr = _devaddr; + } + + // **** Additional functions separate from the original masterchef contract **** + + function setSdtPerBlock(uint256 _sdtPerBlock) public onlyOwner { + require(_sdtPerBlock > 0, "!sdtPerBlock-0"); + + sdtPerBlock = _sdtPerBlock; + } + + function setBonusEndBlock(uint256 _bonusEndBlock) public onlyOwner { + bonusEndBlock = _bonusEndBlock; + } + + function setDevFundDivRate(uint256 _devFundDivRate) public onlyOwner { + require(_devFundDivRate > 0, "!devFundDivRate-0"); + devFundDivRate = _devFundDivRate; + } +} diff --git a/protocol/contracts/governance/TimelockGovernance.sol b/protocol/contracts/governance/TimelockGovernance.sol new file mode 100644 index 0000000..345e83b --- /dev/null +++ b/protocol/contracts/governance/TimelockGovernance.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.7; + +import "../temp/openzeppelin/SafeMath.sol"; + +contract Timelock { + using SafeMath for uint256; + + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewDelay(uint256 indexed newDelay); + event CancelTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event ExecuteTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event QueueTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + + uint256 public constant GRACE_PERIOD = 14 days; + uint256 public constant MINIMUM_DELAY = 8 hours; + uint256 public constant MAXIMUM_DELAY = 30 days; + + address public admin; + address public pendingAdmin; + uint256 public delay; + bool public admin_initialized; + + mapping(bytes32 => bool) public queuedTransactions; + + constructor(address admin_, uint256 delay_) public { + require( + delay_ >= MINIMUM_DELAY, + "Timelock::constructor: Delay must exceed minimum delay." + ); + require( + delay_ <= MAXIMUM_DELAY, + "Timelock::constructor: Delay must not exceed maximum delay." + ); + + admin = admin_; + delay = delay_; + admin_initialized = false; + } + + // XXX: function() external payable { } + receive() external payable {} + + function setDelay(uint256 delay_) public { + require( + msg.sender == address(this), + "Timelock::setDelay: Call must come from Timelock." + ); + require( + delay_ >= MINIMUM_DELAY, + "Timelock::setDelay: Delay must exceed minimum delay." + ); + require( + delay_ <= MAXIMUM_DELAY, + "Timelock::setDelay: Delay must not exceed maximum delay." + ); + delay = delay_; + + emit NewDelay(delay); + } + + function acceptAdmin() public { + require( + msg.sender == pendingAdmin, + "Timelock::acceptAdmin: Call must come from pendingAdmin." + ); + admin = msg.sender; + pendingAdmin = address(0); + + emit NewAdmin(admin); + } + + function setPendingAdmin(address pendingAdmin_) public { + // allows one time setting of admin for deployment purposes + if (admin_initialized) { + require( + msg.sender == address(this), + "Timelock::setPendingAdmin: Call must come from Timelock." + ); + } else { + require( + msg.sender == admin, + "Timelock::setPendingAdmin: First call must come from admin." + ); + admin_initialized = true; + } + pendingAdmin = pendingAdmin_; + + emit NewPendingAdmin(pendingAdmin); + } + + function queueTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public returns (bytes32) { + require( + msg.sender == admin, + "Timelock::queueTransaction: Call must come from admin." + ); + require( + eta >= getBlockTimestamp().add(delay), + "Timelock::queueTransaction: Estimated execution block must satisfy delay." + ); + + bytes32 txHash = keccak256( + abi.encode(target, value, signature, data, eta) + ); + queuedTransactions[txHash] = true; + + emit QueueTransaction(txHash, target, value, signature, data, eta); + return txHash; + } + + function cancelTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public { + require( + msg.sender == admin, + "Timelock::cancelTransaction: Call must come from admin." + ); + + bytes32 txHash = keccak256( + abi.encode(target, value, signature, data, eta) + ); + queuedTransactions[txHash] = false; + + emit CancelTransaction(txHash, target, value, signature, data, eta); + } + + function executeTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public payable returns (bytes memory) { + require( + msg.sender == admin, + "Timelock::executeTransaction: Call must come from admin." + ); + + bytes32 txHash = keccak256( + abi.encode(target, value, signature, data, eta) + ); + require( + queuedTransactions[txHash], + "Timelock::executeTransaction: Transaction hasn't been queued." + ); + require( + getBlockTimestamp() >= eta, + "Timelock::executeTransaction: Transaction hasn't surpassed time lock." + ); + require( + getBlockTimestamp() <= eta.add(GRACE_PERIOD), + "Timelock::executeTransaction: Transaction is stale." + ); + + queuedTransactions[txHash] = false; + + bytes memory callData; + + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked( + bytes4(keccak256(bytes(signature))), + data + ); + } + + // solium-disable-next-line security/no-call-value + (bool success, bytes memory returnData) = target.call{value: value}( + callData + ); + require( + success, + "Timelock::executeTransaction: Transaction execution reverted." + ); + + emit ExecuteTransaction(txHash, target, value, signature, data, eta); + + return returnData; + } + + function getBlockTimestamp() internal view returns (uint256) { + // solium-disable-next-line security/no-block-members + return block.timestamp; + } +} diff --git a/protocol/contracts/liquidations/DarkParadise.sol b/protocol/contracts/liquidations/DarkParadise.sol new file mode 100644 index 0000000..1a68b86 --- /dev/null +++ b/protocol/contracts/liquidations/DarkParadise.sol @@ -0,0 +1,169 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.6.0; + +import "../temp/openzeppelin/IERC20.sol"; +import "../temp/openzeppelin/Context.sol"; +import "../temp/openzeppelin/SafeMath.sol"; +import "../temp/openzeppelin/Address.sol"; +import "../temp/openzeppelin/ERC20.sol"; +import "../temp/openzeppelin/Ownable.sol"; + +abstract contract IStratAccessNft { + function getTotalUseCount(address _account, uint256 _id) + public + view + virtual + returns (uint256); + + function getStratUseCount( + address _account, + uint256 _id, + address _strategy + ) public view virtual returns (uint256); + + function startUsingNFT(address _account, uint256 _id) public virtual; + + function endUsingNFT(address _account, uint256 _id) public virtual; +} + +contract DarkParadise is ERC20("Liquidation SDT", "liqSDT"), Ownable { + using SafeMath for uint256; + IERC20 public sdt; + address public rewardDistribution; + IStratAccessNft public nft; + + // common, rare, unique ids considered in range on 1-111 + uint256 public constant rareMinId = 101; + uint256 public constant uniqueId = 111; + + uint256 public minNFTId = 223; + uint256 public maxNFTId = 444; + + uint256 public commonLimit = 300 * 10**18; + uint256 public rareLimit = 1500 * 10**18; + uint256 public uniqueLimit = 5000 * 10**18; + + mapping(address => uint256) public usedNFT; + + event Stake(address indexed staker, uint256 xsdtReceived); + event Unstake(address indexed unstaker, uint256 sdtReceived); + event RewardDistributorSet(address indexed newRewardDistributor); + event SdtFeeReceived(address indexed from, uint256 sdtAmount); + + modifier onlyRewardDistribution() { + require( + _msgSender() == rewardDistribution, + "Caller is not reward distribution" + ); + _; + } + + constructor(IERC20 _sdt, IStratAccessNft _nft) public { + sdt = _sdt; + nft = _nft; + } + + function getLimit(address user) public view returns (uint256) { + uint256 nftId = usedNFT[user]; + if (nftId == 0) return 0; + + uint256 effectiveId = ((nftId - 1) % 111) + 1; + if (effectiveId < rareMinId) return commonLimit; + if (effectiveId < uniqueId) return rareLimit; + return uniqueLimit; + } + + function enter(uint256 _amount, uint256 _nftId) public { + address sender = _msgSender(); + + if (usedNFT[sender] == 0) { + require(_nftId >= minNFTId && _nftId <= maxNFTId, "Invalid nft"); + usedNFT[sender] = _nftId; + nft.startUsingNFT(sender, _nftId); + } + + uint256 totalSdt = sdt.balanceOf(address(this)); + uint256 totalShares = totalSupply(); + if (totalShares == 0 || totalSdt == 0) { + require(_amount <= getLimit(sender), "Limit hit"); + _mint(sender, _amount); + emit Stake(sender, _amount); + } else { + uint256 effectiveSdtBal = + balanceOf(sender).mul(totalSdt).div(totalShares); + require( + effectiveSdtBal.add(_amount) <= getLimit(sender), + "Limit hit" + ); + uint256 sharesToMint = _amount.mul(totalShares).div(totalSdt); + _mint(sender, sharesToMint); + emit Stake(sender, sharesToMint); + } + sdt.transferFrom(sender, address(this), _amount); + } + + function leave(uint256 _share) public { + address sender = _msgSender(); + uint256 totalShares = totalSupply(); + uint256 what = + _share.mul(sdt.balanceOf(address(this))).div(totalShares); + _burn(sender, _share); + sdt.transfer(sender, what); + + if (balanceOf(sender) == 0) { + uint256 nftId = usedNFT[sender]; + usedNFT[sender] = 0; + nft.endUsingNFT(sender, nftId); + } + + emit Unstake(sender, what); + } + + function setRewardDistribution(address _rewardDistribution) + external + onlyOwner + { + rewardDistribution = _rewardDistribution; + emit RewardDistributorSet(_rewardDistribution); + } + + function notifyRewardAmount(uint256 _balance) + external + onlyRewardDistribution + { + sdt.transferFrom(_msgSender(), address(this), _balance); + emit SdtFeeReceived(_msgSender(), _balance); + } + + function setNFT(IStratAccessNft _nft) public onlyOwner { + nft = _nft; + } + + function setMinMaxNFT(uint256 _min, uint256 _max) external onlyOwner { + if (minNFTId != _min) minNFTId = _min; + if (maxNFTId != _max) maxNFTId = _max; + } + + function setDepositLimits( + uint256 _common, + uint256 _rare, + uint256 _unique + ) external onlyOwner { + if (commonLimit != _common) commonLimit = _common; + if (rareLimit != _rare) rareLimit = _rare; + if (uniqueLimit != _unique) uniqueLimit = _unique; + } + + function transfer(address, uint256) public override returns (bool) { + revert("Restricted"); + } + + function transferFrom( + address, + address, + uint256 + ) public override returns (bool) { + revert("Restricted"); + } +} diff --git a/protocol/contracts/liquidations/DeathGod.sol b/protocol/contracts/liquidations/DeathGod.sol new file mode 100644 index 0000000..8b525c5 --- /dev/null +++ b/protocol/contracts/liquidations/DeathGod.sol @@ -0,0 +1,349 @@ +pragma solidity ^0.5.17; + +pragma experimental ABIEncoderV2; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20Detailed.sol"; +import "@openzeppelinV2/contracts/ownership/Ownable.sol"; + +import "../../interfaces/uniswap/Uni.sol"; +import "../../interfaces/flashLoan/IERC3156FlashBorrower.sol"; +import "../../interfaces/flashLoan/IERC3156FlashLender.sol"; +import "../../interfaces/archer/ITipJar.sol"; + +interface ILendingPool { + function liquidationCall( + address _collateral, + address _reserve, + address _user, + uint256 _purchaseAmount, + bool _receiveAToken + ) external payable; +} + +interface ILendingPoolAddressesProvider { + function getLendingPool() external view returns (address); +} + +contract DeathGod is IERC3156FlashBorrower { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + enum Action { + NORMAL, + OTHER + } + + address public governance; + mapping(address => bool) public keepers; + address public darkParadise; + address public constant uni = + address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + address public constant sdt = + address(0x73968b9a57c6E53d41345FD57a6E6ae27d6CDB2F); + address public lendingPoolAddressProvider = + address(0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5); + address public treasury = + address(0x9D75C85f864Ab9149E23F27C35addaE09B9B909C); + + uint256 public performanceFee = 2000; + uint256 public constant FEE_DENOMINATOR = 10000; + + IERC3156FlashLender lender; + ITipJar public tipJar; + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + function() external payable {} + + constructor( + address _keeper, + address _darkParadise, + IERC3156FlashLender _lender, + address _tipJar + ) public { + governance = msg.sender; + keepers[_keeper] = true; + darkParadise = _darkParadise; + lender = _lender; + tipJar = ITipJar(_tipJar); + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function setAaveLendingPoolAddressProvider( + address _lendingPoolAddressProvider + ) external onlyGovernance { + lendingPoolAddressProvider = _lendingPoolAddressProvider; + } + + function setLender(IERC3156FlashLender _lender) external onlyGovernance { + lender = _lender; + } + + function setTipJar(address _tipJar) external onlyGovernance { + tipJar = ITipJar(_tipJar); + } + + function setDarkParadise(address _darkParadise) external onlyGovernance { + darkParadise = _darkParadise; + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function addKeeper(address _keeper) external onlyGovernance { + keepers[_keeper] = true; + } + + function removeKeeper(address _keeper) external onlyGovernance { + keepers[_keeper] = false; + } + + function sendSDTToDarkParadise(address _token, uint256 _amount) + public + payable + { + require( + msg.sender == governance || keepers[msg.sender] == true, + "Not authorised" + ); + require(msg.value > 0, "tip amount must be > 0"); + require( + _amount <= IERC20(_token).balanceOf(address(this)), + "Not enough tokens" + ); + // pay tip in ETH to miner + tipJar.tip.value(msg.value)(); + + IERC20(_token).safeApprove(uni, _amount); + address[] memory path = new address[](3); + path[0] = _token; + path[1] = weth; + path[2] = sdt; + + uint256 _sdtBefore = IERC20(sdt).balanceOf(address(this)); + Uni(uni).swapExactTokensForTokens( + _amount, + uint256(0), + path, + address(this), + now.add(1800) + ); + uint256 _sdtAfter = IERC20(sdt).balanceOf(address(this)); + + IERC20(sdt).safeTransfer(darkParadise, _sdtAfter.sub(_sdtBefore)); + } + + /// @dev ERC-3156 Flash loan callback + function onFlashLoan( + address initiator, + address token, + uint256 amount, + uint256 fee, + bytes calldata data + ) external returns (bytes32) { + require( + msg.sender == address(lender), + "FlashBorrower: Untrusted lender" + ); + require( + initiator == address(this), + "FlashBorrower: Untrusted loan initiator" + ); + // Action action = abi.decode(data, (Action)); + ( + Action action, + address _collateralAsset, + address _debtAsset, + address _user, + uint256 _debtToCover, + bool _receiveaToken, + uint256 _minerTipPct + ) = abi.decode( + data, + (Action, address, address, address, uint256, bool, uint256) + ); + if (action == Action.NORMAL) { + useFlashLoan( + _collateralAsset, + _debtAsset, + _user, + _debtToCover, + _receiveaToken, + _minerTipPct + ); + } else if (action == Action.OTHER) { + // do another + } + return keccak256("ERC3156FlashBorrower.onFlashLoan"); + } + + function useFlashLoan( + address _collateralAsset, + address _debtAsset, + address _user, + uint256 _debtToCover, + bool _receiveaToken, + uint256 _minerTipPct + ) private { + ILendingPool lendingPool = ILendingPool( + ILendingPoolAddressesProvider(lendingPoolAddressProvider) + .getLendingPool() + ); + require( + IERC20(_debtAsset).approve(address(lendingPool), _debtToCover), + "Approval error" + ); + + // uint256 _ethBefore = address(this).balance; + uint256 collateralBefore = IERC20(_collateralAsset).balanceOf( + address(this) + ); + // Calling liquidate() on AAVE. Assumes this contract already has `_debtToCover` amount of `_debtAsset` + lendingPool.liquidationCall( + _collateralAsset, + _debtAsset, + _user, + _debtToCover, + _receiveaToken + ); + uint256 collateralAfter = IERC20(_collateralAsset).balanceOf( + address(this) + ); + // uint256 _ethAfter = address(this).balance; + + // Swapping ETH to USDC + IERC20(_collateralAsset).safeApprove( + uni, + collateralAfter.sub(collateralBefore) + ); + address[] memory path = new address[](2); + path[0] = _collateralAsset; + path[1] = _debtAsset; + + uint256 _debtAssetBefore = IERC20(_debtAsset).balanceOf(address(this)); + Uni(uni).swapExactTokensForTokens( + collateralAfter.sub(collateralBefore), + uint256(0), + path, + address(this), + now.add(1800) + ); + uint256 _debtAssetAfter = IERC20(_debtAsset).balanceOf(address(this)); + + // liquidation profit = Net USDC later - FlashLoaned USDC - FlashFee + uint256 profit = _debtAssetAfter + .sub(_debtAssetBefore) + .sub(_debtToCover) + .sub(lender.flashFee(_debtAsset, _debtToCover)); + // _minerTipPct % of liquidation profit to miner + tipMinerInToken( + _debtAsset, + profit.mul(_minerTipPct).div(10000), + _collateralAsset + ); + // sending performance fees to treasury + sendFeeToTreasury(_debtAsset); + } + + // _minerTipPct: 2500 for 25% of liquidation profits + function liquidateOnAave( + address _collateralAsset, + address _debtAsset, + address _user, + uint256 _debtToCover, + bool _receiveaToken, + uint256 _minerTipPct + ) public payable { + require(keepers[msg.sender] == true, "Not a keeper"); + // taking flash-loan + // bytes memory data = abi.encode(_collateralAsset, _debtAsset, _user, _debtToCover, _receiveaToken, _minerTipPct); + flashBorrow( + _debtAsset, + _debtToCover, + _collateralAsset, + _user, + _receiveaToken, + _minerTipPct + ); + } + + /// @dev Initiate a flash loan + function flashBorrow( + address _token, + uint256 _amount, + address _collateralAsset, + address _user, + bool _receiveaToken, + uint256 _minerTipPct + ) private { + bytes memory data = abi.encode( + Action.NORMAL, + _collateralAsset, + _token, + _user, + _amount, + _receiveaToken, + _minerTipPct + ); + uint256 _allowance = IERC20(_token).allowance( + address(this), + address(lender) + ); + uint256 _fee = lender.flashFee(_token, _amount); + uint256 _repayment = _amount + _fee; + IERC20(_token).approve(address(lender), _allowance + _repayment); + lender.flashLoan(this, _token, _amount, data); + } + + function tipMinerInToken( + address _tipToken, + uint256 _tipAmount, + address _collateralAsset + ) private { + // swapping miner's profit USDC to ETH + IERC20(_tipToken).safeApprove(uni, _tipAmount); + address[] memory path = new address[](2); + path[0] = _tipToken; + path[1] = _collateralAsset; + + uint256 _ethBefore = address(this).balance; + Uni(uni).swapExactTokensForETH( + _tipAmount, + uint256(0), + path, + address(this), + now.add(1800) + ); + uint256 _ethAfter = address(this).balance; + // sending tip in ETH to miner + tipJar.tip.value(_ethAfter.sub(_ethBefore))(); + } + + function sendFeeToTreasury(address _debtAsset) private { + uint256 _debtAssetRemaining = IERC20(_debtAsset).balanceOf( + address(this) + ); + uint256 _fee = _debtAssetRemaining.mul(performanceFee).div( + FEE_DENOMINATOR + ); + IERC20(_debtAsset).safeTransfer(treasury, _fee); + } +} diff --git a/protocol/contracts/nft/ERC1155Tradable.sol b/protocol/contracts/nft/ERC1155Tradable.sol new file mode 100644 index 0000000..8c957df --- /dev/null +++ b/protocol/contracts/nft/ERC1155Tradable.sol @@ -0,0 +1,1208 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.0; + +import "@openzeppelinV2/contracts/math/Math.sol"; +import "@openzeppelinV2/contracts/GSN/Context.sol"; +import "@openzeppelinV2/contracts/ownership/Ownable.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; + +/** + * @title Roles + * @dev Library for managing addresses assigned to a Role. + */ +library Roles { + struct Role { + mapping(address => bool) bearer; + } + + /** + * @dev Give an account access to this role. + */ + function add(Role storage role, address account) internal { + require(!has(role, account), "Roles: account already has role"); + role.bearer[account] = true; + } + + /** + * @dev Remove an account's access to this role. + */ + function remove(Role storage role, address account) internal { + require(has(role, account), "Roles: account does not have role"); + role.bearer[account] = false; + } + + /** + * @dev Check if an account has this role. + * @return bool + */ + function has(Role storage role, address account) + internal + view + returns (bool) + { + require(account != address(0), "Roles: account is the zero address"); + return role.bearer[account]; + } +} + +contract MinterRole is Context { + using Roles for Roles.Role; + + event MinterAdded(address indexed account); + event MinterRemoved(address indexed account); + + Roles.Role private _minters; + + constructor() internal { + _addMinter(_msgSender()); + } + + modifier onlyMinter() { + require( + isMinter(_msgSender()), + "MinterRole: caller does not have the Minter role" + ); + _; + } + + function isMinter(address account) public view returns (bool) { + return _minters.has(account); + } + + function addMinter(address account) public onlyMinter { + _addMinter(account); + } + + function renounceMinter() public { + _removeMinter(_msgSender()); + } + + function _addMinter(address account) internal { + _minters.add(account); + emit MinterAdded(account); + } + + function _removeMinter(address account) internal { + _minters.remove(account); + emit MinterRemoved(account); + } +} + +/** + * @title WhitelistAdminRole + * @dev WhitelistAdmins are responsible for assigning and removing Whitelisted accounts. + */ +contract WhitelistAdminRole is Context { + using Roles for Roles.Role; + + event WhitelistAdminAdded(address indexed account); + event WhitelistAdminRemoved(address indexed account); + + Roles.Role private _whitelistAdmins; + + constructor() internal { + _addWhitelistAdmin(_msgSender()); + } + + modifier onlyWhitelistAdmin() { + require( + isWhitelistAdmin(_msgSender()), + "WhitelistAdminRole: caller does not have the WhitelistAdmin role" + ); + _; + } + + function isWhitelistAdmin(address account) public view returns (bool) { + return _whitelistAdmins.has(account); + } + + function addWhitelistAdmin(address account) public onlyWhitelistAdmin { + _addWhitelistAdmin(account); + } + + function renounceWhitelistAdmin() public { + _removeWhitelistAdmin(_msgSender()); + } + + function _addWhitelistAdmin(address account) internal { + _whitelistAdmins.add(account); + emit WhitelistAdminAdded(account); + } + + function _removeWhitelistAdmin(address account) internal { + _whitelistAdmins.remove(account); + emit WhitelistAdminRemoved(account); + } +} + +/** + * @title ERC165 + * @dev https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md + */ +interface IERC165 { + /** + * @notice Query if a contract implements an interface + * @dev Interface identification is specified in ERC-165. This function + * uses less than 30,000 gas + * @param _interfaceId The interface identifier, as specified in ERC-165 + */ + function supportsInterface(bytes4 _interfaceId) + external + view + returns (bool); +} + +/** + * @dev ERC-1155 interface for accepting safe transfers. + */ +interface IERC1155TokenReceiver { + /** + * @notice Handle the receipt of a single ERC1155 token type + * @dev An ERC1155-compliant smart contract MUST call this function on the token recipient contract, at the end of a `safeTransferFrom` after the balance has been updated + * This function MAY throw to revert and reject the transfer + * Return of other amount than the magic value MUST result in the transaction being reverted + * Note: The token contract address is always the message sender + * @param _operator The address which called the `safeTransferFrom` function + * @param _from The address which previously owned the token + * @param _id The id of the token being transferred + * @param _amount The amount of tokens being transferred + * @param _data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + */ + function onERC1155Received( + address _operator, + address _from, + uint256 _id, + uint256 _amount, + bytes calldata _data + ) external returns (bytes4); + + /** + * @notice Handle the receipt of multiple ERC1155 token types + * @dev An ERC1155-compliant smart contract MUST call this function on the token recipient contract, at the end of a `safeBatchTransferFrom` after the balances have been updated + * This function MAY throw to revert and reject the transfer + * Return of other amount than the magic value WILL result in the transaction being reverted + * Note: The token contract address is always the message sender + * @param _operator The address which called the `safeBatchTransferFrom` function + * @param _from The address which previously owned the token + * @param _ids An array containing ids of each token being transferred + * @param _amounts An array containing amounts of each token being transferred + * @param _data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _amounts, + bytes calldata _data + ) external returns (bytes4); + + /** + * @notice Indicates whether a contract implements the `ERC1155TokenReceiver` functions and so can accept ERC1155 token types. + * @param interfaceID The ERC-165 interface ID that is queried for support.s + * @dev This function MUST return true if it implements the ERC1155TokenReceiver interface and ERC-165 interface. + * This function MUST NOT consume more than 5,000 gas. + * @return Wheter ERC-165 or ERC1155TokenReceiver interfaces are supported. + */ + function supportsInterface(bytes4 interfaceID) external view returns (bool); +} + +interface IERC1155 { + // Events + + /** + * @dev Either TransferSingle or TransferBatch MUST emit when tokens are transferred, including zero amount transfers as well as minting or burning + * Operator MUST be msg.sender + * When minting/creating tokens, the `_from` field MUST be set to `0x0` + * When burning/destroying tokens, the `_to` field MUST be set to `0x0` + * The total amount transferred from address 0x0 minus the total amount transferred to 0x0 may be used by clients and exchanges to be added to the "circulating supply" for a given token ID + * To broadcast the existence of a token ID with no initial balance, the contract SHOULD emit the TransferSingle event from `0x0` to `0x0`, with the token creator as `_operator`, and a `_amount` of 0 + */ + event TransferSingle( + address indexed _operator, + address indexed _from, + address indexed _to, + uint256 _id, + uint256 _amount + ); + + /** + * @dev Either TransferSingle or TransferBatch MUST emit when tokens are transferred, including zero amount transfers as well as minting or burning + * Operator MUST be msg.sender + * When minting/creating tokens, the `_from` field MUST be set to `0x0` + * When burning/destroying tokens, the `_to` field MUST be set to `0x0` + * The total amount transferred from address 0x0 minus the total amount transferred to 0x0 may be used by clients and exchanges to be added to the "circulating supply" for a given token ID + * To broadcast the existence of multiple token IDs with no initial balance, this SHOULD emit the TransferBatch event from `0x0` to `0x0`, with the token creator as `_operator`, and a `_amount` of 0 + */ + event TransferBatch( + address indexed _operator, + address indexed _from, + address indexed _to, + uint256[] _ids, + uint256[] _amounts + ); + + /** + * @dev MUST emit when an approval is updated + */ + event ApprovalForAll( + address indexed _owner, + address indexed _operator, + bool _approved + ); + + /** + * @dev MUST emit when the URI is updated for a token ID + * URIs are defined in RFC 3986 + * The URI MUST point a JSON file that conforms to the "ERC-1155 Metadata JSON Schema" + */ + event URI(string _amount, uint256 indexed _id); + + /** + * @notice Transfers amount of an _id from the _from address to the _to address specified + * @dev MUST emit TransferSingle event on success + * Caller must be approved to manage the _from account's tokens (see isApprovedForAll) + * MUST throw if `_to` is the zero address + * MUST throw if balance of sender for token `_id` is lower than the `_amount` sent + * MUST throw on any other error + * When transfer is complete, this function MUST check if `_to` is a smart contract (code size > 0). If so, it MUST call `onERC1155Received` on `_to` and revert if the return amount is not `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bytes calldata _data + ) external; + + /** + * @notice Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @dev MUST emit TransferBatch event on success + * Caller must be approved to manage the _from account's tokens (see isApprovedForAll) + * MUST throw if `_to` is the zero address + * MUST throw if length of `_ids` is not the same as length of `_amounts` + * MUST throw if any of the balance of sender for token `_ids` is lower than the respective `_amounts` sent + * MUST throw on any other error + * When transfer is complete, this function MUST check if `_to` is a smart contract (code size > 0). If so, it MUST call `onERC1155BatchReceived` on `_to` and revert if the return amount is not `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * Transfers and events MUST occur in the array order they were submitted (_ids[0] before _ids[1], etc) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] calldata _ids, + uint256[] calldata _amounts, + bytes calldata _data + ) external; + + /** + * @notice Get the balance of an account's Tokens + * @param _owner The address of the token holder + * @param _id ID of the Token + * @return The _owner's balance of the Token type requested + */ + function balanceOf(address _owner, uint256 _id) + external + view + returns (uint256); + + /** + * @notice Get the balance of multiple account/token pairs + * @param _owners The addresses of the token holders + * @param _ids ID of the Tokens + * @return The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair) + */ + function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) + external + view + returns (uint256[] memory); + + /** + * @notice Enable or disable approval for a third party ("operator") to manage all of caller's tokens + * @dev MUST emit the ApprovalForAll event on success + * @param _operator Address to add to the set of authorized operators + * @param _approved True if the operator is approved, false to revoke approval + */ + function setApprovalForAll(address _operator, bool _approved) external; + + /** + * @notice Queries the approval status of an operator for a given owner + * @param _owner The owner of the Tokens + * @param _operator Address of authorized operator + * @return True if the operator is approved, false if not + */ + function isApprovedForAll(address _owner, address _operator) + external + view + returns (bool isOperator); +} + +/** + * @dev Implementation of Multi-Token Standard contract + */ +contract ERC1155 is IERC165 { + using SafeMath for uint256; + using Address for address; + + /***********************************| + | Variables and Events | + |__________________________________*/ + + // onReceive function signatures + bytes4 internal constant ERC1155_RECEIVED_VALUE = 0xf23a6e61; + bytes4 internal constant ERC1155_BATCH_RECEIVED_VALUE = 0xbc197c81; + + // Objects balances + mapping(address => mapping(uint256 => uint256)) internal balances; + + // Operator Functions + mapping(address => mapping(address => bool)) internal operators; + + // Events + event TransferSingle( + address indexed _operator, + address indexed _from, + address indexed _to, + uint256 _id, + uint256 _amount + ); + event TransferBatch( + address indexed _operator, + address indexed _from, + address indexed _to, + uint256[] _ids, + uint256[] _amounts + ); + event ApprovalForAll( + address indexed _owner, + address indexed _operator, + bool _approved + ); + event URI(string _uri, uint256 indexed _id); + + /***********************************| + | Public Transfer Functions | + |__________________________________*/ + + /** + * @notice Transfers amount amount of an _id from the _from address to the _to address specified + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bytes memory _data + ) public { + require( + (msg.sender == _from) || isApprovedForAll(_from, msg.sender), + "ERC1155#safeTransferFrom: INVALID_OPERATOR" + ); + require( + _to != address(0), + "ERC1155#safeTransferFrom: INVALID_RECIPIENT" + ); + // require(_amount >= balances[_from][_id]) is not necessary since checked with safemath operations + + _safeTransferFrom(_from, _to, _id, _amount); + _callonERC1155Received(_from, _to, _id, _amount, _data); + } + + /** + * @notice Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + * @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) public { + // Requirements + require( + (msg.sender == _from) || isApprovedForAll(_from, msg.sender), + "ERC1155#safeBatchTransferFrom: INVALID_OPERATOR" + ); + require( + _to != address(0), + "ERC1155#safeBatchTransferFrom: INVALID_RECIPIENT" + ); + + _safeBatchTransferFrom(_from, _to, _ids, _amounts); + _callonERC1155BatchReceived(_from, _to, _ids, _amounts, _data); + } + + /***********************************| + | Internal Transfer Functions | + |__________________________________*/ + + /** + * @notice Transfers amount amount of an _id from the _from address to the _to address specified + * @param _from Source address + * @param _to Target address + * @param _id ID of the token type + * @param _amount Transfered amount + */ + function _safeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount + ) internal { + // Update balances + balances[_from][_id] = balances[_from][_id].sub(_amount); // Subtract amount + balances[_to][_id] = balances[_to][_id].add(_amount); // Add amount + + // Emit event + emit TransferSingle(msg.sender, _from, _to, _id, _amount); + } + + /** + * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155Received(...) + */ + function _callonERC1155Received( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bytes memory _data + ) internal { + // Check if recipient is contract + if (_to.isContract()) { + bytes4 retval = IERC1155TokenReceiver(_to).onERC1155Received( + msg.sender, + _from, + _id, + _amount, + _data + ); + require( + retval == ERC1155_RECEIVED_VALUE, + "ERC1155#_callonERC1155Received: INVALID_ON_RECEIVE_MESSAGE" + ); + } + } + + /** + * @notice Send multiple types of Tokens from the _from address to the _to address (with safety call) + * @param _from Source addresses + * @param _to Target addresses + * @param _ids IDs of each token type + * @param _amounts Transfer amounts per token type + */ + function _safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts + ) internal { + require( + _ids.length == _amounts.length, + "ERC1155#_safeBatchTransferFrom: INVALID_ARRAYS_LENGTH" + ); + + // Number of transfer to execute + uint256 nTransfer = _ids.length; + + // Executing all transfers + for (uint256 i = 0; i < nTransfer; i++) { + // Update storage balance of previous bin + balances[_from][_ids[i]] = balances[_from][_ids[i]].sub( + _amounts[i] + ); + balances[_to][_ids[i]] = balances[_to][_ids[i]].add(_amounts[i]); + } + + // Emit event + emit TransferBatch(msg.sender, _from, _to, _ids, _amounts); + } + + /** + * @notice Verifies if receiver is contract and if so, calls (_to).onERC1155BatchReceived(...) + */ + function _callonERC1155BatchReceived( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) internal { + // Pass data if recipient is contract + if (_to.isContract()) { + bytes4 retval = IERC1155TokenReceiver(_to).onERC1155BatchReceived( + msg.sender, + _from, + _ids, + _amounts, + _data + ); + require( + retval == ERC1155_BATCH_RECEIVED_VALUE, + "ERC1155#_callonERC1155BatchReceived: INVALID_ON_RECEIVE_MESSAGE" + ); + } + } + + /***********************************| + | Operator Functions | + |__________________________________*/ + + /** + * @notice Enable or disable approval for a third party ("operator") to manage all of caller's tokens + * @param _operator Address to add to the set of authorized operators + * @param _approved True if the operator is approved, false to revoke approval + */ + function setApprovalForAll(address _operator, bool _approved) external { + // Update operator status + operators[msg.sender][_operator] = _approved; + emit ApprovalForAll(msg.sender, _operator, _approved); + } + + /** + * @notice Queries the approval status of an operator for a given owner + * @param _owner The owner of the Tokens + * @param _operator Address of authorized operator + * @return True if the operator is approved, false if not + */ + function isApprovedForAll(address _owner, address _operator) + public + view + returns (bool isOperator) + { + return operators[_owner][_operator]; + } + + /***********************************| + | Balance Functions | + |__________________________________*/ + + /** + * @notice Get the balance of an account's Tokens + * @param _owner The address of the token holder + * @param _id ID of the Token + * @return The _owner's balance of the Token type requested + */ + function balanceOf(address _owner, uint256 _id) + public + view + returns (uint256) + { + return balances[_owner][_id]; + } + + /** + * @notice Get the balance of multiple account/token pairs + * @param _owners The addresses of the token holders + * @param _ids ID of the Tokens + * @return The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair) + */ + function balanceOfBatch(address[] memory _owners, uint256[] memory _ids) + public + view + returns (uint256[] memory) + { + require( + _owners.length == _ids.length, + "ERC1155#balanceOfBatch: INVALID_ARRAY_LENGTH" + ); + + // Variables + uint256[] memory batchBalances = new uint256[](_owners.length); + + // Iterate over each owner and token ID + for (uint256 i = 0; i < _owners.length; i++) { + batchBalances[i] = balances[_owners[i]][_ids[i]]; + } + + return batchBalances; + } + + /***********************************| + | ERC165 Functions | + |__________________________________*/ + + /** + * INTERFACE_SIGNATURE_ERC165 = bytes4(keccak256("supportsInterface(bytes4)")); + */ + bytes4 private constant INTERFACE_SIGNATURE_ERC165 = 0x01ffc9a7; + + /** + * INTERFACE_SIGNATURE_ERC1155 = + * bytes4(keccak256("safeTransferFrom(address,address,uint256,uint256,bytes)")) ^ + * bytes4(keccak256("safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)")) ^ + * bytes4(keccak256("balanceOf(address,uint256)")) ^ + * bytes4(keccak256("balanceOfBatch(address[],uint256[])")) ^ + * bytes4(keccak256("setApprovalForAll(address,bool)")) ^ + * bytes4(keccak256("isApprovedForAll(address,address)")); + */ + bytes4 private constant INTERFACE_SIGNATURE_ERC1155 = 0xd9b67a26; + + /** + * @notice Query if a contract implements an interface + * @param _interfaceID The interface identifier, as specified in ERC-165 + * @return `true` if the contract implements `_interfaceID` and + */ + function supportsInterface(bytes4 _interfaceID) + external + view + returns (bool) + { + if ( + _interfaceID == INTERFACE_SIGNATURE_ERC165 || + _interfaceID == INTERFACE_SIGNATURE_ERC1155 + ) { + return true; + } + return false; + } +} + +/** + * @notice Contract that handles metadata related methods. + * @dev Methods assume a deterministic generation of URI based on token IDs. + * Methods also assume that URI uses hex representation of token IDs. + */ +contract ERC1155Metadata { + // URI's default URI prefix + string internal baseMetadataURI; + event URI(string _uri, uint256 indexed _id); + + /***********************************| + | Metadata Public Function s | + |__________________________________*/ + + /** + * @notice A distinct Uniform Resource Identifier (URI) for a given token. + * @dev URIs are defined in RFC 3986. + * URIs are assumed to be deterministically generated based on token ID + * Token IDs are assumed to be represented in their hex format in URIs + * @return URI string + */ + function uri(uint256 _id) public view returns (string memory) { + return + string(abi.encodePacked(baseMetadataURI, _uint2str(_id), ".json")); + } + + /***********************************| + | Metadata Internal Functions | + |__________________________________*/ + + /** + * @notice Will emit default URI log event for corresponding token _id + * @param _tokenIDs Array of IDs of tokens to log default URI + */ + function _logURIs(uint256[] memory _tokenIDs) internal { + string memory baseURL = baseMetadataURI; + string memory tokenURI; + + for (uint256 i = 0; i < _tokenIDs.length; i++) { + tokenURI = string( + abi.encodePacked(baseURL, _uint2str(_tokenIDs[i]), ".json") + ); + emit URI(tokenURI, _tokenIDs[i]); + } + } + + /** + * @notice Will emit a specific URI log event for corresponding token + * @param _tokenIDs IDs of the token corresponding to the _uris logged + * @param _URIs The URIs of the specified _tokenIDs + */ + function _logURIs(uint256[] memory _tokenIDs, string[] memory _URIs) + internal + { + require( + _tokenIDs.length == _URIs.length, + "ERC1155Metadata#_logURIs: INVALID_ARRAYS_LENGTH" + ); + for (uint256 i = 0; i < _tokenIDs.length; i++) { + emit URI(_URIs[i], _tokenIDs[i]); + } + } + + /** + * @notice Will update the base URL of token's URI + * @param _newBaseMetadataURI New base URL of token's URI + */ + function _setBaseMetadataURI(string memory _newBaseMetadataURI) internal { + baseMetadataURI = _newBaseMetadataURI; + } + + /***********************************| + | Utility Internal Functions | + |__________________________________*/ + + /** + * @notice Convert uint256 to string + * @param _i Unsigned integer to convert to string + */ + function _uint2str(uint256 _i) + internal + pure + returns (string memory _uintAsString) + { + if (_i == 0) { + return "0"; + } + + uint256 j = _i; + uint256 ii = _i; + uint256 len; + + // Get number of bytes + while (j != 0) { + len++; + j /= 10; + } + + bytes memory bstr = new bytes(len); + uint256 k = len - 1; + + // Get each individual ASCII + while (ii != 0) { + bstr[k--] = bytes1(uint8(48 + (ii % 10))); + ii /= 10; + } + + // Convert to string + return string(bstr); + } +} + +/** + * @dev Multi-Fungible Tokens with minting and burning methods. These methods assume + * a parent contract to be executed as they are `internal` functions + */ +contract ERC1155MintBurn is ERC1155 { + /****************************************| + | Minting Functions | + |_______________________________________*/ + + /** + * @notice Mint _amount of tokens of a given id + * @param _to The address to mint tokens to + * @param _id Token id to mint + * @param _amount The amount to be minted + * @param _data Data to pass if receiver is contract + */ + function _mint( + address _to, + uint256 _id, + uint256 _amount, + bytes memory _data + ) internal { + // Add _amount + balances[_to][_id] = balances[_to][_id].add(_amount); + + // Emit event + emit TransferSingle(msg.sender, address(0x0), _to, _id, _amount); + + // Calling onReceive method if recipient is contract + _callonERC1155Received(address(0x0), _to, _id, _amount, _data); + } + + /** + * @notice Mint tokens for each ids in _ids + * @param _to The address to mint tokens to + * @param _ids Array of ids to mint + * @param _amounts Array of amount of tokens to mint per id + * @param _data Data to pass if receiver is contract + */ + function _batchMint( + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) internal { + require( + _ids.length == _amounts.length, + "ERC1155MintBurn#batchMint: INVALID_ARRAYS_LENGTH" + ); + + // Number of mints to execute + uint256 nMint = _ids.length; + + // Executing all minting + for (uint256 i = 0; i < nMint; i++) { + // Update storage balance + balances[_to][_ids[i]] = balances[_to][_ids[i]].add(_amounts[i]); + } + + // Emit batch mint event + emit TransferBatch(msg.sender, address(0x0), _to, _ids, _amounts); + + // Calling onReceive method if recipient is contract + _callonERC1155BatchReceived(address(0x0), _to, _ids, _amounts, _data); + } + + /****************************************| + | Burning Functions | + |_______________________________________*/ + + /** + * @notice Burn _amount of tokens of a given token id + * @param _from The address to burn tokens from + * @param _id Token id to burn + * @param _amount The amount to be burned + */ + function _burn( + address _from, + uint256 _id, + uint256 _amount + ) internal { + //Substract _amount + balances[_from][_id] = balances[_from][_id].sub(_amount); + + // Emit event + emit TransferSingle(msg.sender, _from, address(0x0), _id, _amount); + } + + /** + * @notice Burn tokens of given token id for each (_ids[i], _amounts[i]) pair + * @param _from The address to burn tokens from + * @param _ids Array of token ids to burn + * @param _amounts Array of the amount to be burned + */ + function _batchBurn( + address _from, + uint256[] memory _ids, + uint256[] memory _amounts + ) internal { + require( + _ids.length == _amounts.length, + "ERC1155MintBurn#batchBurn: INVALID_ARRAYS_LENGTH" + ); + + // Number of mints to execute + uint256 nBurn = _ids.length; + + // Executing all minting + for (uint256 i = 0; i < nBurn; i++) { + // Update storage balance + balances[_from][_ids[i]] = balances[_from][_ids[i]].sub( + _amounts[i] + ); + } + + // Emit batch mint event + emit TransferBatch(msg.sender, _from, address(0x0), _ids, _amounts); + } +} + +library Strings { + // via https://github.com/oraclize/ethereum-api/blob/master/oraclizeAPI_0.5.sol + function strConcat( + string memory _a, + string memory _b, + string memory _c, + string memory _d, + string memory _e + ) internal pure returns (string memory) { + bytes memory _ba = bytes(_a); + bytes memory _bb = bytes(_b); + bytes memory _bc = bytes(_c); + bytes memory _bd = bytes(_d); + bytes memory _be = bytes(_e); + string memory abcde = new string( + _ba.length + _bb.length + _bc.length + _bd.length + _be.length + ); + bytes memory babcde = bytes(abcde); + uint256 k = 0; + for (uint256 i = 0; i < _ba.length; i++) babcde[k++] = _ba[i]; + for (uint256 i = 0; i < _bb.length; i++) babcde[k++] = _bb[i]; + for (uint256 i = 0; i < _bc.length; i++) babcde[k++] = _bc[i]; + for (uint256 i = 0; i < _bd.length; i++) babcde[k++] = _bd[i]; + for (uint256 i = 0; i < _be.length; i++) babcde[k++] = _be[i]; + return string(babcde); + } + + function strConcat( + string memory _a, + string memory _b, + string memory _c, + string memory _d + ) internal pure returns (string memory) { + return strConcat(_a, _b, _c, _d, ""); + } + + function strConcat( + string memory _a, + string memory _b, + string memory _c + ) internal pure returns (string memory) { + return strConcat(_a, _b, _c, "", ""); + } + + function strConcat(string memory _a, string memory _b) + internal + pure + returns (string memory) + { + return strConcat(_a, _b, "", "", ""); + } + + function uint2str(uint256 _i) + internal + pure + returns (string memory _uintAsString) + { + if (_i == 0) { + return "0"; + } + uint256 j = _i; + uint256 len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint256 k = len - 1; + while (_i != 0) { + bstr[k--] = bytes1(uint8(48 + (_i % 10))); + _i /= 10; + } + return string(bstr); + } +} + +contract OwnableDelegateProxy {} + +contract ProxyRegistry { + mapping(address => OwnableDelegateProxy) public proxies; +} + +/** + * @title ERC1155Tradable + * ERC1155Tradable - ERC1155 contract that whitelists an operator address, + * has create and mint functionality, and supports useful standards from OpenZeppelin, + like _exists(), name(), symbol(), and totalSupply() + */ +contract ERC1155Tradable is + ERC1155, + ERC1155MintBurn, + ERC1155Metadata, + Ownable, + MinterRole, + WhitelistAdminRole +{ + using Strings for string; + + address proxyRegistryAddress; + uint256 internal _currentTokenID = 0; + mapping(uint256 => address) public creators; + mapping(uint256 => uint256) public tokenSupply; + mapping(uint256 => uint256) public tokenMaxSupply; + // Contract name + string public name; + // Contract symbol + string public symbol; + + constructor( + string memory _name, + string memory _symbol, + address _proxyRegistryAddress + ) public { + name = _name; + symbol = _symbol; + proxyRegistryAddress = _proxyRegistryAddress; + } + + function removeWhitelistAdmin(address account) public onlyOwner { + _removeWhitelistAdmin(account); + } + + function removeMinter(address account) public onlyOwner { + _removeMinter(account); + } + + function uri(uint256 _id) public view returns (string memory) { + require(_exists(_id), "ERC721Tradable#uri: NONEXISTENT_TOKEN"); + return Strings.strConcat(baseMetadataURI, Strings.uint2str(_id)); + } + + /** + * @dev Returns the total quantity for a token ID + * @param _id uint256 ID of the token to query + * @return amount of token in existence + */ + function totalSupply(uint256 _id) public view returns (uint256) { + return tokenSupply[_id]; + } + + /** + * @dev Returns the max quantity for a token ID + * @param _id uint256 ID of the token to query + * @return amount of token in existence + */ + function maxSupply(uint256 _id) public view returns (uint256) { + return tokenMaxSupply[_id]; + } + + /** + * @dev Will update the base URL of token's URI + * @param _newBaseMetadataURI New base URL of token's URI + */ + function setBaseMetadataURI(string memory _newBaseMetadataURI) + public + onlyWhitelistAdmin + { + _setBaseMetadataURI(_newBaseMetadataURI); + } + + /** + * @dev Creates a new token type and assigns _initialSupply to an address + * @param _maxSupply max supply allowed + * @param _initialSupply Optional amount to supply the first owner + * @param _uri Optional URI for this token type + * @param _data Optional data to pass if receiver is contract + * @return The newly created token ID + */ + function create( + uint256 _maxSupply, + uint256 _initialSupply, + string calldata _uri, + bytes calldata _data + ) external onlyWhitelistAdmin returns (uint256 tokenId) { + require( + _initialSupply <= _maxSupply, + "Initial supply cannot be more than max supply" + ); + uint256 _id = _getNextTokenID(); + _incrementTokenTypeId(); + creators[_id] = msg.sender; + + if (bytes(_uri).length > 0) { + emit URI(_uri, _id); + } + + if (_initialSupply != 0) _mint(msg.sender, _id, _initialSupply, _data); + tokenSupply[_id] = _initialSupply; + tokenMaxSupply[_id] = _maxSupply; + return _id; + } + + /** + * @dev Mints some amount of tokens to an address + * @param _to Address of the future owner of the token + * @param _id Token ID to mint + * @param _quantity Amount of tokens to mint + * @param _data Data to pass if receiver is contract + */ + function mint( + address _to, + uint256 _id, + uint256 _quantity, + bytes memory _data + ) public onlyMinter { + uint256 tokenId = _id; + require( + tokenSupply[tokenId] < tokenMaxSupply[tokenId], + "Max supply reached" + ); + _mint(_to, _id, _quantity, _data); + tokenSupply[_id] = tokenSupply[_id].add(_quantity); + } + + /** + * @dev Mints some amount of tokens to an address + * @param _to The address to mint tokens to + * @param _ids Array of ids to mint + * @param _amounts Array of amount of tokens to mint per id + * @param _data Data to pass if receiver is contract + */ + function batchMint( + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) public onlyMinter { + uint256 nMints = _ids.length; + for (uint256 i = 0; i < nMints; i++) { + uint256 tokenId = _ids[i]; + require( + tokenSupply[tokenId] < tokenMaxSupply[tokenId], + "Max supply reached" + ); + tokenSupply[tokenId] = tokenSupply[tokenId].add(_amounts[i]); + } + _batchMint(_to, _ids, _amounts, _data); + } + + /** + * @notice Burn _amount of tokens of a given token id + * @param _from The address to burn tokens from + * @param _id Token id to burn + * @param _amount The amount to be burned + */ + function burn( + address _from, + uint256 _id, + uint256 _amount + ) public onlyOwner { + tokenSupply[_id] = tokenSupply[_id].sub(_amount); + _burn(_from, _id, _amount); + } + + /** + * Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-free listings. + */ + function isApprovedForAll(address _owner, address _operator) + public + view + returns (bool isOperator) + { + // Whitelist OpenSea proxy contract for easy trading. + ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); + if (address(proxyRegistry.proxies(_owner)) == _operator) { + return true; + } + + return ERC1155.isApprovedForAll(_owner, _operator); + } + + /** + * @dev Returns whether the specified token exists by checking to see if it has a creator + * @param _id uint256 ID of the token to query the existence of + * @return bool whether the token exists + */ + function _exists(uint256 _id) internal view returns (bool) { + return creators[_id] != address(0); + } + + /** + * @dev calculates the next token ID based on value of _currentTokenID + * @return uint256 for the next token ID + */ + function _getNextTokenID() private view returns (uint256) { + return _currentTokenID.add(1); + } + + /** + * @dev increments the value of _currentTokenID + */ + function _incrementTokenTypeId() private { + _currentTokenID++; + } +} diff --git a/protocol/contracts/nft/NFTBoosterVault.sol b/protocol/contracts/nft/NFTBoosterVault.sol new file mode 100644 index 0000000..a41cc0a --- /dev/null +++ b/protocol/contracts/nft/NFTBoosterVault.sol @@ -0,0 +1,91 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.0; + +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/ownership/Ownable.sol"; +import {IERC1155TokenReceiver, IERC1155} from "./ERC1155Tradable.sol"; + +contract NFTBoosterVault is IERC1155TokenReceiver, Ownable { + using SafeMath for uint256; + + IERC1155 private nft; + mapping(address => uint256) private stakedNFT; + + event Staked(address indexed user, uint256 indexed nftId); + event Unstaked(address indexed user, uint256 indexed nftId); + + constructor(address _nft) public { + nft = IERC1155(_nft); + } + + function getNFTAddress() external view returns (address) { + return address(nft); + } + + function getStakedNFT(address user) external view returns (uint256) { + return stakedNFT[user]; + } + + function stake(uint256 _nftId) external { + require(stakedNFT[msg.sender] == 0, "already staked"); + stakedNFT[msg.sender] = _nftId; + emit Staked(msg.sender, _nftId); + nft.safeTransferFrom(msg.sender, address(this), _nftId, 1, ""); + } + + function unstake() external { + uint256 nftId = stakedNFT[msg.sender]; + require(nftId != 0, "not staked"); + stakedNFT[msg.sender] = 0; + emit Unstaked(msg.sender, nftId); + nft.safeTransferFrom(address(this), msg.sender, nftId, 1, ""); + } + + function claimLockedNFTs( + uint256[] calldata _ids, + uint256[] calldata _amounts + ) external onlyOwner { + nft.safeBatchTransferFrom( + address(this), + msg.sender, + _ids, + _amounts, + "" + ); + } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external returns (bytes4) { + // Accept only StakeDAO NFT + if (msg.sender == address(nft)) { + return 0xf23a6e61; + } + revert("nft not accepted"); + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external returns (bytes4) { + revert("batch transfer not accepted"); + } + + function supportsInterface(bytes4 interfaceID) + external + view + returns (bool) + { + if (interfaceID == 0x4e2312e0) { + return true; + } + return false; + } +} diff --git a/protocol/contracts/nft/StakeDaoNFT.sol b/protocol/contracts/nft/StakeDaoNFT.sol new file mode 100644 index 0000000..476063d --- /dev/null +++ b/protocol/contracts/nft/StakeDaoNFT.sol @@ -0,0 +1,24 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.5.0; + +import "./ERC1155Tradable.sol"; + +/** + * @title Stake Dao NFT Contract + */ +contract StakeDaoNFT is ERC1155Tradable { + constructor(address _proxyRegistryAddress) + public + ERC1155Tradable("Stake DAO NFT", "sdNFT", _proxyRegistryAddress) + { + _setBaseMetadataURI( + "https://gateway.pinata.cloud/ipfs/QmZwsoGKw42DxNwnQXKtWiuULSzFrUPCNHUx6yhNgrUMj6/metadata/" + ); + } + + function contractURI() public view returns (string memory) { + return + "https://gateway.pinata.cloud/ipfs/Qmc1i37KPdg7Cp8rzjgp3QoCECaEbfoSymCpKG8hF85ENv"; + } +} diff --git a/protocol/contracts/nft/StakedaoNFTPalace.sol b/protocol/contracts/nft/StakedaoNFTPalace.sol new file mode 100644 index 0000000..05963fe --- /dev/null +++ b/protocol/contracts/nft/StakedaoNFTPalace.sol @@ -0,0 +1,119 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.0; + +import "./ERC1155Tradable.sol"; + +contract StakeTokenWrapper { + using SafeMath for uint256; + IERC20 public xsdt; + + constructor(IERC20 _xsdt) public { + xsdt = IERC20(_xsdt); + } + + uint256 private _totalSupply; + mapping(address => uint256) private _balances; + + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + function stake(uint256 amount) public { + _totalSupply = _totalSupply.add(amount); + _balances[msg.sender] = _balances[msg.sender].add(amount); + xsdt.transferFrom(msg.sender, address(this), amount); + } + + function withdraw(uint256 amount) public { + _totalSupply = _totalSupply.sub(amount); + _balances[msg.sender] = _balances[msg.sender].sub(amount); + xsdt.transfer(msg.sender, amount); + } +} + +contract StakeDaoNFTPalace is StakeTokenWrapper, Ownable { + ERC1155Tradable public nft; + + mapping(address => uint256) public lastUpdateTime; + mapping(address => uint256) public points; + mapping(uint256 => uint256) public cards; + + event CardAdded(uint256 card, uint256 points); + event Staked(address indexed user, uint256 amount); + event Withdrawn(address indexed user, uint256 amount); + event Redeemed(address indexed user, uint256 id); + event NFTSet(ERC1155Tradable indexed newNFT); + + modifier updateReward(address account) { + if (account != address(0)) { + points[account] = earned(account); + lastUpdateTime[account] = block.timestamp; + } + _; + } + + constructor(ERC1155Tradable _nftAddress, IERC20 _xsdt) + public + StakeTokenWrapper(_xsdt) + { + nft = _nftAddress; + } + + function setNFT(ERC1155Tradable _nftAddress) public onlyOwner { + nft = _nftAddress; + emit NFTSet(_nftAddress); + } + + function addCard(uint256 cardId, uint256 amount) public onlyOwner { + cards[cardId] = amount; + emit CardAdded(cardId, amount); + } + + function earned(address account) public view returns (uint256) { + uint256 timeDifference = block.timestamp.sub(lastUpdateTime[account]); + uint256 balance = balanceOf(account); + uint256 decimals = 1e18; + uint256 x = balance / decimals; + uint256 ratePerMin = decimals.mul(x).div(x.add(12000)).div(240); + return points[account].add(ratePerMin.mul(timeDifference)); + } + + // stake visibility is public as overriding StakeTokenWrapper's stake() function + function stake(uint256 amount) public updateReward(msg.sender) { + require(amount > 0, "Invalid amount"); + + super.stake(amount); + emit Staked(msg.sender, amount); + } + + function withdraw(uint256 amount) public updateReward(msg.sender) { + require(amount > 0, "Cannot withdraw 0"); + + super.withdraw(amount); + emit Withdrawn(msg.sender, amount); + } + + function exit() external { + withdraw(balanceOf(msg.sender)); + } + + function redeem(uint256 card) public updateReward(msg.sender) { + require(cards[card] != 0, "Card not found"); + require( + points[msg.sender] >= cards[card], + "Not enough points to redeem for card" + ); + require( + nft.totalSupply(card) < nft.maxSupply(card), + "Max cards minted" + ); + + points[msg.sender] = points[msg.sender].sub(cards[card]); + nft.mint(msg.sender, card, 1, ""); + emit Redeemed(msg.sender, card); + } +} diff --git a/protocol/contracts/nft/StratAccessNFT.sol b/protocol/contracts/nft/StratAccessNFT.sol new file mode 100644 index 0000000..2a8531a --- /dev/null +++ b/protocol/contracts/nft/StratAccessNFT.sol @@ -0,0 +1,200 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.5.0; + +import "./ERC1155Tradable.sol"; +import "@openzeppelinV2/contracts/ownership/Ownable.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/GSN/Context.sol"; + +/** + * @title StrategyRole + * @dev Owner is responsible to add/remove strategy + */ +contract StrategyRole is Context, Ownable { + using Roles for Roles.Role; + + event StrategyAdded(address indexed account); + event StrategyRemoved(address indexed account); + + Roles.Role private _strategies; + + modifier onlyStrategy() { + require( + isStrategy(_msgSender()), + "StrategyRole: caller does not have the Strategy role" + ); + _; + } + + function isStrategy(address account) public view returns (bool) { + return _strategies.has(account); + } + + function addStrategy(address account) public onlyOwner { + _addStrategy(account); + } + + function removeStrategy(address account) public onlyOwner { + _removeStrategy(account); + } + + function _addStrategy(address account) internal { + _strategies.add(account); + emit StrategyAdded(account); + } + + function _removeStrategy(address account) internal { + _strategies.remove(account); + emit StrategyRemoved(account); + } +} + +/** + * @title Strategy Access NFT Contract for StakeDAO + * @dev The contract keeps a count of NFTs being used in some strategy for + * for each user and allows transfers based on that. + */ +contract StratAccessNFT is ERC1155Tradable, StrategyRole { + using SafeMath for uint256; + + event StartedUsingNFT( + address indexed account, + uint256 indexed id, + address indexed strategy + ); + event EndedUsingNFT( + address indexed account, + uint256 indexed id, + address indexed strategy + ); + + // mapping account => nftId => useCount + // this is used to restrict transfers if nft is being used in any strategy + mapping(address => mapping(uint256 => uint256)) internal totalUseCount; + + // mapping account => nftId => strategyAddress => useCount + // this is used to make sure a strategy can only end using nft that it started using before + mapping(address => mapping(uint256 => mapping(address => uint256))) + internal stratUseCount; + + // TODO: proper name, metadata uri + constructor(address _proxyRegistryAddress) + public + ERC1155Tradable("Stake DAO NFT", "sdNFT", _proxyRegistryAddress) + { + _setBaseMetadataURI( + "https://gateway.pinata.cloud/ipfs/QmZwsoGKw42DxNwnQXKtWiuULSzFrUPCNHUx6yhNgrUMj6/metadata/" + ); + + // starting ids for these nfts from 223 + // since 222 nfts (tempest, pythia) have been minted using old implementation + _currentTokenID = 222; + } + + function contractURI() public view returns (string memory) { + return + "https://gateway.pinata.cloud/ipfs/Qmc1i37KPdg7Cp8rzjgp3QoCECaEbfoSymCpKG8hF85ENv"; + } + + function getTotalUseCount(address _account, uint256 _id) + public + view + returns (uint256) + { + return totalUseCount[_account][_id]; + } + + function getStratUseCount( + address _account, + uint256 _id, + address _strategy + ) public view returns (uint256) { + return stratUseCount[_account][_id][_strategy]; + } + + /** + * @notice Mark NFT as being used. Only callable by registered strategies + * @param _account User account address + * @param _id ID of the token type + */ + function startUsingNFT(address _account, uint256 _id) public onlyStrategy { + require( + balances[_account][_id] > 0, + "StratAccessNFT: user account doesnt have NFT" + ); + stratUseCount[_account][_id][msg + .sender] = stratUseCount[_account][_id][msg.sender].add(1); + totalUseCount[_account][_id] = totalUseCount[_account][_id].add(1); + emit StartedUsingNFT(_account, _id, msg.sender); + } + + /** + * @notice Unmark NFT as being used. Only callable by registered strategies + * @param _account User account address + * @param _id ID of the token type + */ + function endUsingNFT(address _account, uint256 _id) public onlyStrategy { + // if a strategy tries to call endUsingNFT function for which it did not call + // startUsingNFT then subtraction reverts due to safemath. + stratUseCount[_account][_id][msg + .sender] = stratUseCount[_account][_id][msg.sender].sub(1); + totalUseCount[_account][_id] = totalUseCount[_account][_id].sub(1); + emit EndedUsingNFT(_account, _id, msg.sender); + } + + /** + * @dev Overrides safeTransferFrom function of ERC1155 to introduce totalUseCount check + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _id, + uint256 _amount, + bytes memory _data + ) public { + // check if nft is being used + require( + totalUseCount[_from][_id] == 0, + "StratAccessNFT: NFT being used in strategy" + ); + super.safeTransferFrom(_from, _to, _id, _amount, _data); + } + + /** + * @dev Overrides safeBatchTransferFrom function of ERC1155 to introduce totalUseCount check + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _amounts, + bytes memory _data + ) public { + // Number of transfer to execute + uint256 nTransfer = _ids.length; + + // check if any nft is being used + for (uint256 i = 0; i < nTransfer; i++) { + require( + totalUseCount[_from][_ids[i]] == 0, + "StratAccessNFT: NFT being used in strategy" + ); + } + + super.safeBatchTransferFrom(_from, _to, _ids, _amounts, _data); + } + + function burn( + address _from, + uint256 _id, + uint256 _amount + ) public onlyOwner { + // check if nft is being used + require( + totalUseCount[_from][_id] == 0, + "StratAccessNFT: NFT being used in strategy" + ); + super.burn(_from, _id, _amount); + } +} diff --git a/protocol/contracts/polygon/MultiCall.sol b/protocol/contracts/polygon/MultiCall.sol new file mode 100644 index 0000000..4a32395 --- /dev/null +++ b/protocol/contracts/polygon/MultiCall.sol @@ -0,0 +1,45 @@ +pragma solidity >=0.5.0; +pragma experimental ABIEncoderV2; + +/// @title Multicall - Aggregate results from multiple read-only function calls +/// @author Michael Elliot +/// @author Joshua Levine +/// @author Nick Johnson + +contract Multicall { + struct Call { + address target; + bytes callData; + } + function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) { + blockNumber = block.number; + returnData = new bytes[](calls.length); + for(uint256 i = 0; i < calls.length; i++) { + (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); + require(success); + returnData[i] = ret; + } + } + // Helper functions + function getEthBalance(address addr) public view returns (uint256 balance) { + balance = addr.balance; + } + function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) { + blockHash = blockhash(blockNumber); + } + function getLastBlockHash() public view returns (bytes32 blockHash) { + blockHash = blockhash(block.number - 1); + } + function getCurrentBlockTimestamp() public view returns (uint256 timestamp) { + timestamp = block.timestamp; + } + function getCurrentBlockDifficulty() public view returns (uint256 difficulty) { + difficulty = block.difficulty; + } + function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { + gaslimit = block.gaslimit; + } + function getCurrentBlockCoinbase() public view returns (address coinbase) { + coinbase = block.coinbase; + } +} \ No newline at end of file diff --git a/protocol/contracts/polygon/WMATIC.sol b/protocol/contracts/polygon/WMATIC.sol new file mode 100644 index 0000000..5690593 --- /dev/null +++ b/protocol/contracts/polygon/WMATIC.sol @@ -0,0 +1,764 @@ +/** + *Submitted for verification at polygonscan.com on 2021-06-09 + */ + +// https://firebird.finance DeFi multi-chain yield farms deployer & DEXs aggregator. + +// Copyright (C) 2015, 2016, 2017 Dapphub + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.4.18; + +contract WMATIC { + string public name = "Wrapped Matic"; + string public symbol = "WMATIC"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + function() public payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + msg.sender.transfer(wad); + Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return this.balance; + } + + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom( + address src, + address dst, + uint256 wad + ) public returns (bool) { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + Transfer(src, dst, wad); + + return true; + } +} + +/* + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +*/ diff --git a/protocol/contracts/polygon/curve/GaugeV2.vy b/protocol/contracts/polygon/curve/GaugeV2.vy new file mode 100644 index 0000000..c413768 --- /dev/null +++ b/protocol/contracts/polygon/curve/GaugeV2.vy @@ -0,0 +1,761 @@ +# @version 0.2.8 +""" +@title Liquidity Gauge v2 +@author Curve Finance +@license MIT +""" + +from vyper.interfaces import ERC20 + +implements: ERC20 + + +interface CRV20: + def future_epoch_time_write() -> uint256: nonpayable + def rate() -> uint256: view + +interface Controller: + def period() -> int128: view + def period_write() -> int128: nonpayable + def period_timestamp(p: int128) -> uint256: view + def gauge_relative_weight(addr: address, time: uint256) -> uint256: view + def voting_escrow() -> address: view + def checkpoint(): nonpayable + def checkpoint_gauge(addr: address): nonpayable + +interface Minter: + def token() -> address: view + def controller() -> address: view + def minted(user: address, gauge: address) -> uint256: view + +interface VotingEscrow: + def user_point_epoch(addr: address) -> uint256: view + def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: address + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + + +MAX_REWARDS: constant(uint256) = 8 +TOKENLESS_PRODUCTION: constant(uint256) = 40 +BOOST_WARMUP: constant(uint256) = 2 * 7 * 86400 +WEEK: constant(uint256) = 604800 + +minter: public(address) +crv_token: public(address) +lp_token: public(address) +controller: public(address) +voting_escrow: public(address) +future_epoch_time: public(uint256) + +balanceOf: public(HashMap[address, uint256]) +totalSupply: public(uint256) +allowances: HashMap[address, HashMap[address, uint256]] + +name: public(String[64]) +symbol: public(String[32]) + +# caller -> recipient -> can deposit? +approved_to_deposit: public(HashMap[address, HashMap[address, bool]]) + +working_balances: public(HashMap[address, uint256]) +working_supply: public(uint256) + +# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint +# All values are kept in units of being multiplied by 1e18 +period: public(int128) +period_timestamp: public(uint256[100000000000000000000000000000]) + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint +integrate_inv_supply: public(uint256[100000000000000000000000000000]) # bump epoch when rate() changes + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint +integrate_inv_supply_of: public(HashMap[address, uint256]) +integrate_checkpoint_of: public(HashMap[address, uint256]) + +# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint +# Units: rate * t = already number of coins per address to issue +integrate_fraction: public(HashMap[address, uint256]) + +inflation_rate: public(uint256) + +# For tracking external rewards +reward_contract: public(address) +reward_tokens: public(address[MAX_REWARDS]) + +# deposit / withdraw / claim +reward_sigs: bytes32 + +# reward token -> integral +reward_integral: public(HashMap[address, uint256]) + +# reward token -> claiming address -> integral +reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) + +admin: public(address) +future_admin: public(address) # Can and will be a smart contract +is_killed: public(bool) + + +@external +def __init__( + _name: String[64], + _symbol: String[32], + _lp_token: address, + _minter: address, + _admin: address, +): + """ + @notice Contract constructor + @param _name Token full name + @param _symbol Token symbol + @param _lp_token Liquidity Pool contract address + @param _minter Minter contract address + @param _admin Admin who can kill the gauge + """ + self.name = _name + self.symbol = _symbol + + crv_token: address = Minter(_minter).token() + controller: address = Minter(_minter).controller() + + self.lp_token = _lp_token + self.minter = _minter + self.admin = _admin + self.crv_token = crv_token + self.controller = controller + self.voting_escrow = Controller(controller).voting_escrow() + + self.period_timestamp[0] = block.timestamp + self.inflation_rate = CRV20(crv_token).rate() + self.future_epoch_time = CRV20(crv_token).future_epoch_time_write() + + +@view +@external +def decimals() -> uint256: + """ + @notice Get the number of decimals for this token + @dev Implemented as a view method to reduce gas costs + @return uint256 decimal places + """ + return 18 + + +@view +@external +def integrate_checkpoint() -> uint256: + return self.period_timestamp[self.period] + + +@internal +def _update_liquidity_limit(addr: address, l: uint256, L: uint256): + """ + @notice Calculate limits which depend on the amount of CRV token per-user. + Effectively it calculates working balances to apply amplification + of CRV production by CRV + @param addr User address + @param l User's amount of liquidity (LP tokens) + @param L Total amount of liquidity (LP tokens) + """ + # To be called after totalSupply is updated + _voting_escrow: address = self.voting_escrow + voting_balance: uint256 = ERC20(_voting_escrow).balanceOf(addr) + voting_total: uint256 = ERC20(_voting_escrow).totalSupply() + + lim: uint256 = l * TOKENLESS_PRODUCTION / 100 + if (voting_total > 0) and (block.timestamp > self.period_timestamp[0] + BOOST_WARMUP): + lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 + + lim = min(l, lim) + old_bal: uint256 = self.working_balances[addr] + self.working_balances[addr] = lim + _working_supply: uint256 = self.working_supply + lim - old_bal + self.working_supply = _working_supply + + log UpdateLiquidityLimit(addr, l, L, lim, _working_supply) + + +@internal +def _checkpoint_rewards(_addr: address, _total_supply: uint256): + """ + @notice Claim pending rewards and checkpoint rewards for a user + """ + if _total_supply == 0: + return + + balances: uint256[MAX_REWARDS] = empty(uint256[MAX_REWARDS]) + reward_tokens: address[MAX_REWARDS] = empty(address[MAX_REWARDS]) + for i in range(MAX_REWARDS): + token: address = self.reward_tokens[i] + if token == ZERO_ADDRESS: + break + reward_tokens[i] = token + balances[i] = ERC20(token).balanceOf(self) + + # claim from reward contract + raw_call(self.reward_contract, slice(self.reward_sigs, 8, 4)) # dev: bad claim sig + + for i in range(MAX_REWARDS): + token: address = reward_tokens[i] + if token == ZERO_ADDRESS: + break + dI: uint256 = 10**18 * (ERC20(token).balanceOf(self) - balances[i]) / _total_supply + if _addr == ZERO_ADDRESS: + if dI != 0: + self.reward_integral[token] += dI + continue + + integral: uint256 = self.reward_integral[token] + dI + if dI != 0: + self.reward_integral[token] = integral + + integral_for: uint256 = self.reward_integral_for[token][_addr] + if integral_for < integral: + claimable: uint256 = self.balanceOf[_addr] * (integral - integral_for) / 10**18 + self.reward_integral_for[token][_addr] = integral + response: Bytes[32] = raw_call( + token, + concat( + method_id("transfer(address,uint256)"), + convert(_addr, bytes32), + convert(claimable, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + + +@internal +def _checkpoint(addr: address): + """ + @notice Checkpoint for a user + @param addr User address + """ + _period: int128 = self.period + _period_time: uint256 = self.period_timestamp[_period] + _integrate_inv_supply: uint256 = self.integrate_inv_supply[_period] + rate: uint256 = self.inflation_rate + new_rate: uint256 = rate + prev_future_epoch: uint256 = self.future_epoch_time + if prev_future_epoch >= _period_time: + _token: address = self.crv_token + self.future_epoch_time = CRV20(_token).future_epoch_time_write() + new_rate = CRV20(_token).rate() + self.inflation_rate = new_rate + + if self.is_killed: + # Stop distributing inflation as soon as killed + rate = 0 + + # Update integral of 1/supply + if block.timestamp > _period_time: + _working_supply: uint256 = self.working_supply + _controller: address = self.controller + Controller(_controller).checkpoint_gauge(self) + prev_week_time: uint256 = _period_time + week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp) + + for i in range(500): + dt: uint256 = week_time - prev_week_time + w: uint256 = Controller(_controller).gauge_relative_weight(self, prev_week_time / WEEK * WEEK) + + if _working_supply > 0: + if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time: + # If we went across one or multiple epochs, apply the rate + # of the first epoch until it ends, and then the rate of + # the last epoch. + # If more than one epoch is crossed - the gauge gets less, + # but that'd meen it wasn't called for more than 1 year + _integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply + rate = new_rate + _integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply + else: + _integrate_inv_supply += rate * w * dt / _working_supply + # On precisions of the calculation + # rate ~= 10e18 + # last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) + # _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) + # The largest loss is at dt = 1 + # Loss is 1e-9 - acceptable + + if week_time == block.timestamp: + break + prev_week_time = week_time + week_time = min(week_time + WEEK, block.timestamp) + + _period += 1 + self.period = _period + self.period_timestamp[_period] = block.timestamp + self.integrate_inv_supply[_period] = _integrate_inv_supply + + # Update user-specific integrals + _working_balance: uint256 = self.working_balances[addr] + self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18 + self.integrate_inv_supply_of[addr] = _integrate_inv_supply + self.integrate_checkpoint_of[addr] = block.timestamp + + +@external +def user_checkpoint(addr: address) -> bool: + """ + @notice Record a checkpoint for `addr` + @param addr User address + @return bool success + """ + assert (msg.sender == addr) or (msg.sender == self.minter) # dev: unauthorized + self._checkpoint(addr) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + return True + + +@external +def claimable_tokens(addr: address) -> uint256: + """ + @notice Get the number of claimable tokens per user + @dev This function should be manually changed to "view" in the ABI + @return uint256 number of claimable tokens per user + """ + self._checkpoint(addr) + return self.integrate_fraction[addr] - Minter(self.minter).minted(addr, self) + + +@external +@nonreentrant('lock') +def claimable_reward(_addr: address, _token: address) -> uint256: + """ + @notice Get the number of claimable reward tokens for a user + @dev This function should be manually changed to "view" in the ABI + Calling it via a transaction will claim available reward tokens + @param _addr Account to get reward amount for + @param _token Token to get reward amount for + @return uint256 Claimable reward token amount + """ + claimable: uint256 = ERC20(_token).balanceOf(_addr) + if self.reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_addr, self.totalSupply) + claimable = ERC20(_token).balanceOf(_addr) - claimable + + integral: uint256 = self.reward_integral[_token] + integral_for: uint256 = self.reward_integral_for[_token][_addr] + + if integral_for < integral: + claimable += self.balanceOf[_addr] * (integral - integral_for) / 10**18 + + return claimable + + +@external +@nonreentrant('lock') +def claim_rewards(_addr: address = msg.sender): + """ + @notice Claim available reward tokens for `_addr` + @param _addr Address to claim for + """ + self._checkpoint_rewards(_addr, self.totalSupply) + + +@external +@nonreentrant('lock') +def claim_historic_rewards(_reward_tokens: address[MAX_REWARDS], _addr: address = msg.sender): + """ + @notice Claim reward tokens available from a previously-set staking contract + @param _reward_tokens Array of reward token addresses to claim + @param _addr Address to claim for + """ + for token in _reward_tokens: + if token == ZERO_ADDRESS: + break + integral: uint256 = self.reward_integral[token] + integral_for: uint256 = self.reward_integral_for[token][_addr] + + if integral_for < integral: + claimable: uint256 = self.balanceOf[_addr] * (integral - integral_for) / 10**18 + self.reward_integral_for[token][_addr] = integral + response: Bytes[32] = raw_call( + token, + concat( + method_id("transfer(address,uint256)"), + convert(_addr, bytes32), + convert(claimable, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + + +@external +def kick(addr: address): + """ + @notice Kick `addr` for abusing their boost + @dev Only if either they had another voting event, or their voting escrow lock expired + @param addr Address to kick + """ + _voting_escrow: address = self.voting_escrow + t_last: uint256 = self.integrate_checkpoint_of[addr] + t_ve: uint256 = VotingEscrow(_voting_escrow).user_point_history__ts( + addr, VotingEscrow(_voting_escrow).user_point_epoch(addr) + ) + _balance: uint256 = self.balanceOf[addr] + + assert ERC20(self.voting_escrow).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed + assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed + + self._checkpoint(addr) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + + +@external +def set_approve_deposit(addr: address, can_deposit: bool): + """ + @notice Set whether `addr` can deposit tokens for `msg.sender` + @param addr Address to set approval on + @param can_deposit bool - can this account deposit for `msg.sender`? + """ + self.approved_to_deposit[addr][msg.sender] = can_deposit + + +@external +@nonreentrant('lock') +def deposit(_value: uint256, _addr: address = msg.sender): + """ + @notice Deposit `_value` LP tokens + @dev Depositting also claims pending reward tokens + @param _value Number of tokens to deposit + @param _addr Address to deposit for + """ + if _addr != msg.sender: + assert self.approved_to_deposit[msg.sender][_addr], "Not approved" + + self._checkpoint(_addr) + + if _value != 0: + reward_contract: address = self.reward_contract + total_supply: uint256 = self.totalSupply + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_addr, total_supply) + + total_supply += _value + new_balance: uint256 = self.balanceOf[_addr] + _value + self.balanceOf[_addr] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(_addr, new_balance, total_supply) + + ERC20(self.lp_token).transferFrom(msg.sender, self, _value) + if reward_contract != ZERO_ADDRESS: + deposit_sig: Bytes[4] = slice(self.reward_sigs, 0, 4) + if convert(deposit_sig, uint256) != 0: + raw_call( + reward_contract, + concat(deposit_sig, convert(_value, bytes32)) + ) + + log Deposit(_addr, _value) + log Transfer(ZERO_ADDRESS, _addr, _value) + + +@external +@nonreentrant('lock') +def withdraw(_value: uint256): + """ + @notice Withdraw `_value` LP tokens + @dev Withdrawing also claims pending reward tokens + @param _value Number of tokens to withdraw + """ + self._checkpoint(msg.sender) + + if _value != 0: + reward_contract: address = self.reward_contract + total_supply: uint256 = self.totalSupply + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(msg.sender, total_supply) + + total_supply -= _value + new_balance: uint256 = self.balanceOf[msg.sender] - _value + self.balanceOf[msg.sender] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(msg.sender, new_balance, total_supply) + + if reward_contract != ZERO_ADDRESS: + withdraw_sig: Bytes[4] = slice(self.reward_sigs, 4, 4) + if convert(withdraw_sig, uint256) != 0: + raw_call( + reward_contract, + concat(withdraw_sig, convert(_value, bytes32)) + ) + ERC20(self.lp_token).transfer(msg.sender, _value) + + log Withdraw(msg.sender, _value) + log Transfer(msg.sender, ZERO_ADDRESS, _value) + + +@view +@external +def allowance(_owner : address, _spender : address) -> uint256: + """ + @notice Check the amount of tokens that an owner allowed to a spender + @param _owner The address which owns the funds + @param _spender The address which will spend the funds + @return uint256 Amount of tokens still available for the spender + """ + return self.allowances[_owner][_spender] + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + self._checkpoint(_from) + self._checkpoint(_to) + reward_contract: address = self.reward_contract + + if _value != 0: + total_supply: uint256 = self.totalSupply + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_from, total_supply) + new_balance: uint256 = self.balanceOf[_from] - _value + self.balanceOf[_from] = new_balance + self._update_liquidity_limit(_from, new_balance, total_supply) + + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_to, total_supply) + new_balance = self.balanceOf[_to] + _value + self.balanceOf[_to] = new_balance + self._update_liquidity_limit(_to, new_balance, total_supply) + + log Transfer(_from, _to, _value) + + +@external +@nonreentrant('lock') +def transfer(_to : address, _value : uint256) -> bool: + """ + @notice Transfer token for a specified address + @dev Transferring claims pending reward tokens for the sender and receiver + @param _to The address to transfer to. + @param _value The amount to be transferred. + """ + self._transfer(msg.sender, _to, _value) + + return True + + +@external +@nonreentrant('lock') +def transferFrom(_from : address, _to : address, _value : uint256) -> bool: + """ + @notice Transfer tokens from one address to another. + @dev Transferring claims pending reward tokens for the sender and receiver + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + """ + _allowance: uint256 = self.allowances[_from][msg.sender] + if _allowance != MAX_UINT256: + self.allowances[_from][msg.sender] = _allowance - _value + + self._transfer(_from, _to, _value) + + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @notice Approve the passed address to transfer the specified amount of + tokens on behalf of msg.sender + @dev Beware that changing an allowance via this method brings the risk + that someone may use both the old and new allowance by unfortunate + transaction ordering. This may be mitigated with the use of + {incraseAllowance} and {decreaseAllowance}. + https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will transfer the funds + @param _value The amount of tokens that may be transferred + @return bool success + """ + self.allowances[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + + return True + + +@external +def increaseAllowance(_spender: address, _added_value: uint256) -> bool: + """ + @notice Increase the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _added_value The amount of to increase the allowance + @return bool success + """ + allowance: uint256 = self.allowances[msg.sender][_spender] + _added_value + self.allowances[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + + +@external +def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: + """ + @notice Decrease the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _subtracted_value The amount of to decrease the allowance + @return bool success + """ + allowance: uint256 = self.allowances[msg.sender][_spender] - _subtracted_value + self.allowances[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + + +@external +@nonreentrant('lock') +def set_rewards(_reward_contract: address, _sigs: bytes32, _reward_tokens: address[MAX_REWARDS]): + """ + @notice Set the active reward contract + @dev A reward contract cannot be set while this contract has no deposits + @param _reward_contract Reward contract address. Set to ZERO_ADDRESS to + disable staking. + @param _sigs Four byte selectors for staking, withdrawing and claiming, + right padded with zero bytes. If the reward contract can + be claimed from but does not require staking, the staking + and withdraw selectors should be set to 0x00 + @param _reward_tokens List of claimable tokens for this reward contract + """ + assert msg.sender == self.admin + + lp_token: address = self.lp_token + current_reward_contract: address = self.reward_contract + total_supply: uint256 = self.totalSupply + if current_reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(ZERO_ADDRESS, total_supply) + withdraw_sig: Bytes[4] = slice(self.reward_sigs, 4, 4) + if convert(withdraw_sig, uint256) != 0: + if total_supply != 0: + raw_call( + current_reward_contract, + concat(withdraw_sig, convert(total_supply, bytes32)) + ) + ERC20(lp_token).approve(current_reward_contract, 0) + + if _reward_contract != ZERO_ADDRESS: + assert _reward_contract.is_contract # dev: not a contract + sigs: bytes32 = _sigs + deposit_sig: Bytes[4] = slice(sigs, 0, 4) + withdraw_sig: Bytes[4] = slice(sigs, 4, 4) + + if convert(deposit_sig, uint256) != 0: + # need a non-zero total supply to verify the sigs + assert total_supply != 0 # dev: zero total supply + ERC20(lp_token).approve(_reward_contract, MAX_UINT256) + + # it would be Very Bad if we get the signatures wrong here, so + # we do a test deposit and withdrawal prior to setting them + raw_call( + _reward_contract, + concat(deposit_sig, convert(total_supply, bytes32)) + ) # dev: failed deposit + assert ERC20(lp_token).balanceOf(self) == 0 + raw_call( + _reward_contract, + concat(withdraw_sig, convert(total_supply, bytes32)) + ) # dev: failed withdraw + assert ERC20(lp_token).balanceOf(self) == total_supply + + # deposit and withdraw are good, time to make the actual deposit + raw_call( + _reward_contract, + concat(deposit_sig, convert(total_supply, bytes32)) + ) + else: + assert convert(withdraw_sig, uint256) == 0 # dev: withdraw without deposit + + self.reward_contract = _reward_contract + self.reward_sigs = _sigs + for i in range(MAX_REWARDS): + if _reward_tokens[i] != ZERO_ADDRESS: + self.reward_tokens[i] = _reward_tokens[i] + elif self.reward_tokens[i] != ZERO_ADDRESS: + self.reward_tokens[i] = ZERO_ADDRESS + else: + assert i != 0 # dev: no reward token + break + + if _reward_contract != ZERO_ADDRESS: + # do an initial checkpoint to verify that claims are working + self._checkpoint_rewards(ZERO_ADDRESS, total_supply) + + +@external +def set_killed(_is_killed: bool): + """ + @notice Set the killed status for this contract + @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV + @param _is_killed Killed status to set + """ + assert msg.sender == self.admin + + self.is_killed = _is_killed + + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of GaugeController to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + log ApplyOwnership(_admin) \ No newline at end of file diff --git a/protocol/contracts/polygon/curve/StableSwapAave.vy b/protocol/contracts/polygon/curve/StableSwapAave.vy new file mode 100644 index 0000000..5b802a5 --- /dev/null +++ b/protocol/contracts/polygon/curve/StableSwapAave.vy @@ -0,0 +1,1053 @@ +# @version 0.2.8 +""" +@title Curve aPool +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020 - all rights reserved +@notice Pool implementation with aToken-style lending +""" + +from vyper.interfaces import ERC20 + + +interface LendingPool: + def withdraw(_underlying_asset: address, _amount: uint256, _receiver: address): nonpayable + +interface CurveToken: + def mint(_to: address, _value: uint256) -> bool: nonpayable + def burnFrom(_to: address, _value: uint256) -> bool: nonpayable + + +# Events +event TokenExchange: + buyer: indexed(address) + sold_id: int128 + tokens_sold: uint256 + bought_id: int128 + tokens_bought: uint256 + +event TokenExchangeUnderlying: + buyer: indexed(address) + sold_id: int128 + tokens_sold: uint256 + bought_id: int128 + tokens_bought: uint256 + +event AddLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fees: uint256[N_COINS] + invariant: uint256 + token_supply: uint256 + +event RemoveLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fees: uint256[N_COINS] + token_supply: uint256 + +event RemoveLiquidityOne: + provider: indexed(address) + token_amount: uint256 + coin_amount: uint256 + +event RemoveLiquidityImbalance: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fees: uint256[N_COINS] + invariant: uint256 + token_supply: uint256 + +event CommitNewAdmin: + deadline: indexed(uint256) + admin: indexed(address) + +event NewAdmin: + admin: indexed(address) + +event CommitNewFee: + deadline: indexed(uint256) + fee: uint256 + admin_fee: uint256 + offpeg_fee_multiplier: uint256 + +event NewFee: + fee: uint256 + admin_fee: uint256 + offpeg_fee_multiplier: uint256 + +event RampA: + old_A: uint256 + new_A: uint256 + initial_time: uint256 + future_time: uint256 + +event StopRampA: + A: uint256 + t: uint256 + + +# These constants must be set prior to compiling +N_COINS: constant(int128) = 3 +PRECISION_MUL: constant(uint256[N_COINS]) = [1, 1000000000000, 1000000000000] + +# fixed constants +FEE_DENOMINATOR: constant(uint256) = 10 ** 10 +PRECISION: constant(uint256) = 10 ** 18 # The precision to convert to + +MAX_ADMIN_FEE: constant(uint256) = 10 * 10 ** 9 +MAX_FEE: constant(uint256) = 5 * 10 ** 9 + +MAX_A: constant(uint256) = 10 ** 6 +MAX_A_CHANGE: constant(uint256) = 10 +A_PRECISION: constant(uint256) = 100 + +ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 +MIN_RAMP_TIME: constant(uint256) = 86400 + +coins: public(address[N_COINS]) +underlying_coins: public(address[N_COINS]) +admin_balances: public(uint256[N_COINS]) + +fee: public(uint256) # fee * 1e10 +offpeg_fee_multiplier: public(uint256) # * 1e10 +admin_fee: public(uint256) # admin_fee * 1e10 + +owner: public(address) +lp_token: public(address) + +aave_lending_pool: address +aave_referral: uint256 + +initial_A: public(uint256) +future_A: public(uint256) +initial_A_time: public(uint256) +future_A_time: public(uint256) + +admin_actions_deadline: public(uint256) +transfer_ownership_deadline: public(uint256) +future_fee: public(uint256) +future_admin_fee: public(uint256) +future_offpeg_fee_multiplier: public(uint256) # * 1e10 +future_owner: public(address) + +is_killed: bool +kill_deadline: uint256 +KILL_DEADLINE_DT: constant(uint256) = 2 * 30 * 86400 + + +@external +def __init__( + _coins: address[N_COINS], + _underlying_coins: address[N_COINS], + _pool_token: address, + _aave_lending_pool: address, + _A: uint256, + _fee: uint256, + _admin_fee: uint256, + _offpeg_fee_multiplier: uint256, +): + """ + @notice Contract constructor + @param _coins List of wrapped coin addresses + @param _underlying_coins List of underlying coin addresses + @param _pool_token Pool LP token address + @param _aave_lending_pool Aave lending pool address + @param _A Amplification coefficient multiplied by n * (n - 1) + @param _fee Swap fee expressed as an integer with 1e10 precision + @param _admin_fee Percentage of fee taken as an admin fee, + expressed as an integer with 1e10 precision + @param _offpeg_fee_multiplier Offpeg fee multiplier + """ + for i in range(N_COINS): + assert _coins[i] != ZERO_ADDRESS + assert _underlying_coins[i] != ZERO_ADDRESS + + self.coins = _coins + self.underlying_coins = _underlying_coins + self.initial_A = _A * A_PRECISION + self.future_A = _A * A_PRECISION + self.fee = _fee + self.admin_fee = _admin_fee + self.offpeg_fee_multiplier = _offpeg_fee_multiplier + self.owner = msg.sender + self.kill_deadline = block.timestamp + KILL_DEADLINE_DT + self.lp_token = _pool_token + self.aave_lending_pool = _aave_lending_pool + + # approve transfer of underlying coin to aave lending pool + for coin in _underlying_coins: + _response: Bytes[32] = raw_call( + coin, + concat( + method_id("approve(address,uint256)"), + convert(_aave_lending_pool, bytes32), + convert(MAX_UINT256, bytes32) + ), + max_outsize=32 + ) + if len(_response) != 0: + assert convert(_response, bool) + + + +@view +@internal +def _A() -> uint256: + t1: uint256 = self.future_A_time + A1: uint256 = self.future_A + + if block.timestamp < t1: + # handle ramping up and down of A + A0: uint256 = self.initial_A + t0: uint256 = self.initial_A_time + # Expressions in uint256 cannot have negative numbers, thus "if" + if A1 > A0: + return A0 + (A1 - A0) * (block.timestamp - t0) / (t1 - t0) + else: + return A0 - (A0 - A1) * (block.timestamp - t0) / (t1 - t0) + + else: # when t1 == 0 or block.timestamp >= t1 + return A1 + + +@view +@external +def A() -> uint256: + return self._A() / A_PRECISION + + +@view +@external +def A_precise() -> uint256: + return self._A() + + +@pure +@internal +def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256, _feemul: uint256) -> uint256: + if _feemul <= FEE_DENOMINATOR: + return _fee + else: + xps2: uint256 = (xpi + xpj) + xps2 *= xps2 # Doing just ** 2 can overflow apparently + return (_feemul * _fee) / ( + (_feemul - FEE_DENOMINATOR) * 4 * xpi * xpj / xps2 + \ + FEE_DENOMINATOR) + + +@view +@external +def dynamic_fee(i: int128, j: int128) -> uint256: + """ + @notice Return the fee for swapping between `i` and `j` + @param i Index value for the coin to send + @param j Index value of the coin to recieve + @return Swap fee expressed as an integer with 1e10 precision + """ + precisions: uint256[N_COINS] = PRECISION_MUL + xpi: uint256 = (ERC20(self.coins[i]).balanceOf(self) - self.admin_balances[i]) * precisions[i] + xpj: uint256 = (ERC20(self.coins[j]).balanceOf(self) - self.admin_balances[j]) * precisions[j] + return self._dynamic_fee(xpi, xpj, self.fee, self.offpeg_fee_multiplier) + + +@view +@external +def balances(i: uint256) -> uint256: + """ + @notice Get the current balance of a coin within the + pool, less the accrued admin fees + @param i Index value for the coin to query balance of + @return Token balance + """ + return ERC20(self.coins[i]).balanceOf(self) - self.admin_balances[i] + + +@view +@internal +def _balances() -> uint256[N_COINS]: + result: uint256[N_COINS] = empty(uint256[N_COINS]) + for i in range(N_COINS): + result[i] = ERC20(self.coins[i]).balanceOf(self) - self.admin_balances[i] + return result + + +@pure +@internal +def get_D(xp: uint256[N_COINS], amp: uint256) -> uint256: + """ + D invariant calculation in non-overflowing integer operations + iteratively + + A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) + + Converging solution: + D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1) + """ + S: uint256 = 0 + + for _x in xp: + S += _x + if S == 0: + return 0 + + Dprev: uint256 = 0 + D: uint256 = S + Ann: uint256 = amp * N_COINS + for _i in range(255): + D_P: uint256 = D + for _x in xp: + D_P = D_P * D / (_x * N_COINS + 1) # +1 is to prevent /0 + Dprev = D + D = (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P) + # Equality with the precision of 1 + if D > Dprev: + if D - Dprev <= 1: + return D + else: + if Dprev - D <= 1: + return D + # convergence typically occurs in 4 rounds or less, this should be unreachable! + # if it does happen the pool is borked and LPs can withdraw via `remove_liquidity` + raise + + + +@view +@internal +def get_D_precisions(coin_balances: uint256[N_COINS], amp: uint256) -> uint256: + xp: uint256[N_COINS] = PRECISION_MUL + for i in range(N_COINS): + xp[i] *= coin_balances[i] + return self.get_D(xp, amp) + + +@view +@external +def get_virtual_price() -> uint256: + """ + @notice The current virtual price of the pool LP token + @dev Useful for calculating profits + @return LP token virtual price normalized to 1e18 + """ + D: uint256 = self.get_D_precisions(self._balances(), self._A()) + # D is in the units similar to DAI (e.g. converted to precision 1e18) + # When balanced, D = n * x_u - total virtual value of the portfolio + token_supply: uint256 = ERC20(self.lp_token).totalSupply() + return D * PRECISION / token_supply + + +@view +@external +def calc_token_amount(_amounts: uint256[N_COINS], is_deposit: bool) -> uint256: + """ + @notice Calculate addition or reduction in token supply from a deposit or withdrawal + @dev This calculation accounts for slippage, but not fees. + Needed to prevent front-running, not for precise calculations! + @param _amounts Amount of each coin being deposited + @param is_deposit set True for deposits, False for withdrawals + @return Expected amount of LP tokens received + """ + coin_balances: uint256[N_COINS] = self._balances() + amp: uint256 = self._A() + D0: uint256 = self.get_D_precisions(coin_balances, amp) + for i in range(N_COINS): + if is_deposit: + coin_balances[i] += _amounts[i] + else: + coin_balances[i] -= _amounts[i] + D1: uint256 = self.get_D_precisions(coin_balances, amp) + token_amount: uint256 = ERC20(self.lp_token).totalSupply() + diff: uint256 = 0 + if is_deposit: + diff = D1 - D0 + else: + diff = D0 - D1 + return diff * token_amount / D0 + + +@external +@nonreentrant('lock') +def add_liquidity(_amounts: uint256[N_COINS], _min_mint_amount: uint256, _use_underlying: bool = False) -> uint256: + """ + @notice Deposit coins into the pool + @param _amounts List of amounts of coins to deposit + @param _min_mint_amount Minimum amount of LP tokens to mint from the deposit + @param _use_underlying If True, deposit underlying assets instead of aTokens + @return Amount of LP tokens received by depositing + """ + + assert not self.is_killed # dev: is killed + + # Initial invariant + amp: uint256 = self._A() + old_balances: uint256[N_COINS] = self._balances() + lp_token: address = self.lp_token + token_supply: uint256 = ERC20(lp_token).totalSupply() + D0: uint256 = 0 + if token_supply != 0: + D0 = self.get_D_precisions(old_balances, amp) + + new_balances: uint256[N_COINS] = old_balances + for i in range(N_COINS): + if token_supply == 0: + assert _amounts[i] != 0 # dev: initial deposit requires all coins + new_balances[i] += _amounts[i] + + # Invariant after change + D1: uint256 = self.get_D_precisions(new_balances, amp) + assert D1 > D0 + + # We need to recalculate the invariant accounting for fees + # to calculate fair user's share + fees: uint256[N_COINS] = empty(uint256[N_COINS]) + mint_amount: uint256 = 0 + if token_supply != 0: + # Only account for fees if we are not the first to deposit + ys: uint256 = (D0 + D1) / N_COINS + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + _feemul: uint256 = self.offpeg_fee_multiplier + _admin_fee: uint256 = self.admin_fee + difference: uint256 = 0 + for i in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] / D0 + new_balance: uint256 = new_balances[i] + if ideal_balance > new_balance: + difference = ideal_balance - new_balance + else: + difference = new_balance - ideal_balance + xs: uint256 = old_balances[i] + new_balance + fees[i] = self._dynamic_fee(xs, ys, _fee, _feemul) * difference / FEE_DENOMINATOR + if _admin_fee != 0: + self.admin_balances[i] += fees[i] * _admin_fee / FEE_DENOMINATOR + new_balances[i] = new_balance - fees[i] + D2: uint256 = self.get_D_precisions(new_balances, amp) + mint_amount = token_supply * (D2 - D0) / D0 + else: + mint_amount = D1 # Take the dust if there was any + + assert mint_amount >= _min_mint_amount, "Slippage screwed you" + + # Take coins from the sender + if _use_underlying: + lending_pool: address = self.aave_lending_pool + aave_referral: bytes32 = convert(self.aave_referral, bytes32) + + # Take coins from the sender + for i in range(N_COINS): + amount: uint256 = _amounts[i] + if amount != 0: + coin: address = self.underlying_coins[i] + # transfer underlying coin from msg.sender to self + _response: Bytes[32] = raw_call( + coin, + concat( + method_id("transferFrom(address,address,uint256)"), + convert(msg.sender, bytes32), + convert(self, bytes32), + convert(amount, bytes32) + ), + max_outsize=32 + ) + if len(_response) != 0: + assert convert(_response, bool) + + # deposit to aave lending pool + raw_call( + lending_pool, + concat( + method_id("deposit(address,uint256,address,uint16)"), + convert(coin, bytes32), + convert(amount, bytes32), + convert(self, bytes32), + aave_referral, + ) + ) + else: + for i in range(N_COINS): + amount: uint256 = _amounts[i] + if amount != 0: + assert ERC20(self.coins[i]).transferFrom(msg.sender, self, amount) # dev: failed transfer + + # Mint pool tokens + CurveToken(lp_token).mint(msg.sender, mint_amount) + + log AddLiquidity(msg.sender, _amounts, fees, D1, token_supply + mint_amount) + + return mint_amount + + +@view +@internal +def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS]) -> uint256: + """ + Calculate x[j] if one makes x[i] = x + + Done by solving quadratic equation iteratively. + x_1**2 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + # x in the input is converted to the same price/precision + + assert i != j # dev: same coin + assert j >= 0 # dev: j below zero + assert j < N_COINS # dev: j above N_COINS + + # should be unreachable, but good for safety + assert i >= 0 + assert i < N_COINS + + amp: uint256 = self._A() + D: uint256 = self.get_D(xp, amp) + Ann: uint256 = amp * N_COINS + c: uint256 = D + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + + for _i in range(N_COINS): + if _i == i: + _x = x + elif _i != j: + _x = xp[_i] + else: + continue + S_ += _x + c = c * D / (_x * N_COINS) + c = c * D * A_PRECISION / (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION / Ann # - D + y: uint256 = D + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@view +@internal +def _get_dy(i: int128, j: int128, dx: uint256) -> uint256: + xp: uint256[N_COINS] = self._balances() + precisions: uint256[N_COINS] = PRECISION_MUL + for k in range(N_COINS): + xp[k] *= precisions[k] + + x: uint256 = xp[i] + dx * precisions[i] + y: uint256 = self.get_y(i, j, x, xp) + dy: uint256 = (xp[j] - y) / precisions[j] + _fee: uint256 = self._dynamic_fee( + (xp[i] + x) / 2, (xp[j] + y) / 2, self.fee, self.offpeg_fee_multiplier + ) * dy / FEE_DENOMINATOR + return dy - _fee + + +@view +@external +def get_dy(i: int128, j: int128, dx: uint256) -> uint256: + return self._get_dy(i, j, dx) + + +@view +@external +def get_dy_underlying(i: int128, j: int128, dx: uint256) -> uint256: + return self._get_dy(i, j, dx) + + +@internal +def _exchange(i: int128, j: int128, dx: uint256) -> uint256: + assert not self.is_killed # dev: is killed + # dx and dy are in aTokens + + xp: uint256[N_COINS] = self._balances() + precisions: uint256[N_COINS] = PRECISION_MUL + for k in range(N_COINS): + xp[k] *= precisions[k] + + x: uint256 = xp[i] + dx * precisions[i] + y: uint256 = self.get_y(i, j, x, xp) + dy: uint256 = xp[j] - y + dy_fee: uint256 = dy * self._dynamic_fee( + (xp[i] + x) / 2, (xp[j] + y) / 2, self.fee, self.offpeg_fee_multiplier + ) / FEE_DENOMINATOR + + admin_fee: uint256 = self.admin_fee + if admin_fee != 0: + dy_admin_fee: uint256 = dy_fee * admin_fee / FEE_DENOMINATOR + if dy_admin_fee != 0: + self.admin_balances[j] += dy_admin_fee / precisions[j] + + return (dy - dy_fee) / precisions[j] + + +@external +@nonreentrant('lock') +def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256: + """ + @notice Perform an exchange between two coins + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index valie of the coin to recieve + @param dx Amount of `i` being exchanged + @param min_dy Minimum amount of `j` to receive + @return Actual amount of `j` received + """ + dy: uint256 = self._exchange(i, j, dx) + assert dy >= min_dy, "Exchange resulted in fewer coins than expected" + + assert ERC20(self.coins[i]).transferFrom(msg.sender, self, dx) + assert ERC20(self.coins[j]).transfer(msg.sender, dy) + + log TokenExchange(msg.sender, i, dx, j, dy) + + return dy + + +@external +@nonreentrant('lock') +def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256: + """ + @notice Perform an exchange between two underlying coins + @dev Index values can be found via the `underlying_coins` public getter method + @param i Index value for the underlying coin to send + @param j Index valie of the underlying coin to recieve + @param dx Amount of `i` being exchanged + @param min_dy Minimum amount of `j` to receive + @return Actual amount of `j` received + """ + dy: uint256 = self._exchange(i, j, dx) + assert dy >= min_dy, "Exchange resulted in fewer coins than expected" + + u_coin_i: address = self.underlying_coins[i] + lending_pool: address = self.aave_lending_pool + + # transfer underlying coin from msg.sender to self + _response: Bytes[32] = raw_call( + u_coin_i, + concat( + method_id("transferFrom(address,address,uint256)"), + convert(msg.sender, bytes32), + convert(self, bytes32), + convert(dx, bytes32) + ), + max_outsize=32 + ) + if len(_response) != 0: + assert convert(_response, bool) + + # deposit to aave lending pool + raw_call( + lending_pool, + concat( + method_id("deposit(address,uint256,address,uint16)"), + convert(u_coin_i, bytes32), + convert(dx, bytes32), + convert(self, bytes32), + convert(self.aave_referral, bytes32), + ) + ) + # withdraw `j` underlying from lending pool and transfer to caller + LendingPool(lending_pool).withdraw(self.underlying_coins[j], dy, msg.sender) + + log TokenExchangeUnderlying(msg.sender, i, dx, j, dy) + + return dy + + +@external +@nonreentrant('lock') +def remove_liquidity( + _amount: uint256, + _min_amounts: uint256[N_COINS], + _use_underlying: bool = False, +) -> uint256[N_COINS]: + """ + @notice Withdraw coins from the pool + @dev Withdrawal amounts are based on current deposit ratios + @param _amount Quantity of LP tokens to burn in the withdrawal + @param _min_amounts Minimum amounts of underlying coins to receive + @param _use_underlying If True, withdraw underlying assets instead of aTokens + @return List of amounts of coins that were withdrawn + """ + amounts: uint256[N_COINS] = self._balances() + lp_token: address = self.lp_token + total_supply: uint256 = ERC20(lp_token).totalSupply() + CurveToken(lp_token).burnFrom(msg.sender, _amount) # dev: insufficient funds + + lending_pool: address = ZERO_ADDRESS + if _use_underlying: + lending_pool = self.aave_lending_pool + + for i in range(N_COINS): + value: uint256 = amounts[i] * _amount / total_supply + assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected" + amounts[i] = value + if _use_underlying: + LendingPool(lending_pool).withdraw(self.underlying_coins[i], value, msg.sender) + else: + assert ERC20(self.coins[i]).transfer(msg.sender, value) + + log RemoveLiquidity(msg.sender, amounts, empty(uint256[N_COINS]), total_supply - _amount) + + return amounts + + +@external +@nonreentrant('lock') +def remove_liquidity_imbalance( + _amounts: uint256[N_COINS], + _max_burn_amount: uint256, + _use_underlying: bool = False +) -> uint256: + """ + @notice Withdraw coins from the pool in an imbalanced amount + @param _amounts List of amounts of underlying coins to withdraw + @param _max_burn_amount Maximum amount of LP token to burn in the withdrawal + @param _use_underlying If True, withdraw underlying assets instead of aTokens + @return Actual amount of the LP token burned in the withdrawal + """ + assert not self.is_killed # dev: is killed + + amp: uint256 = self._A() + old_balances: uint256[N_COINS] = self._balances() + D0: uint256 = self.get_D_precisions(old_balances, amp) + new_balances: uint256[N_COINS] = old_balances + for i in range(N_COINS): + new_balances[i] -= _amounts[i] + D1: uint256 = self.get_D_precisions(new_balances, amp) + ys: uint256 = (D0 + D1) / N_COINS + + lp_token: address = self.lp_token + token_supply: uint256 = ERC20(lp_token).totalSupply() + assert token_supply != 0 # dev: zero total supply + + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + _feemul: uint256 = self.offpeg_fee_multiplier + _admin_fee: uint256 = self.admin_fee + fees: uint256[N_COINS] = empty(uint256[N_COINS]) + for i in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] / D0 + new_balance: uint256 = new_balances[i] + difference: uint256 = 0 + if ideal_balance > new_balance: + difference = ideal_balance - new_balance + else: + difference = new_balance - ideal_balance + xs: uint256 = new_balance + old_balances[i] + fees[i] = self._dynamic_fee(xs, ys, _fee, _feemul) * difference / FEE_DENOMINATOR + if _admin_fee != 0: + self.admin_balances[i] += fees[i] * _admin_fee / FEE_DENOMINATOR + new_balances[i] -= fees[i] + D2: uint256 = self.get_D_precisions(new_balances, amp) + + token_amount: uint256 = (D0 - D2) * token_supply / D0 + assert token_amount != 0 # dev: zero tokens burned + assert token_amount <= _max_burn_amount, "Slippage screwed you" + + CurveToken(lp_token).burnFrom(msg.sender, token_amount) # dev: insufficient funds + + lending_pool: address = ZERO_ADDRESS + if _use_underlying: + lending_pool = self.aave_lending_pool + + for i in range(N_COINS): + amount: uint256 = _amounts[i] + if amount != 0: + if _use_underlying: + LendingPool(lending_pool).withdraw(self.underlying_coins[i], amount, msg.sender) + else: + assert ERC20(self.coins[i]).transfer(msg.sender, amount) + + log RemoveLiquidityImbalance(msg.sender, _amounts, fees, D1, token_supply - token_amount) + + return token_amount + + +@pure +@internal +def get_y_D(A_: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: + """ + Calculate x[i] if one reduces D from being calculated for xp to D + + Done by solving quadratic equation iteratively. + x_1**2 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + # x in the input is converted to the same price/precision + + assert i >= 0 # dev: i below zero + assert i < N_COINS # dev: i above N_COINS + + Ann: uint256 = A_ * N_COINS + c: uint256 = D + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + + for _i in range(N_COINS): + if _i != i: + _x = xp[_i] + else: + continue + S_ += _x + c = c * D / (_x * N_COINS) + c = c * D * A_PRECISION / (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION / Ann + y: uint256 = D + + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@view +@internal +def _calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: + # First, need to calculate + # * Get current D + # * Solve Eqn against y_i for D - _token_amount + amp: uint256 = self._A() + xp: uint256[N_COINS] = self._balances() + precisions: uint256[N_COINS] = PRECISION_MUL + + for j in range(N_COINS): + xp[j] *= precisions[j] + + D0: uint256 = self.get_D(xp, amp) + D1: uint256 = D0 - _token_amount * D0 / ERC20(self.lp_token).totalSupply() + new_y: uint256 = self.get_y_D(amp, i, xp, D1) + + xp_reduced: uint256[N_COINS] = xp + ys: uint256 = (D0 + D1) / (2 * N_COINS) + + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + feemul: uint256 = self.offpeg_fee_multiplier + for j in range(N_COINS): + dx_expected: uint256 = 0 + xavg: uint256 = 0 + if j == i: + dx_expected = xp[j] * D1 / D0 - new_y + xavg = (xp[j] + new_y) / 2 + else: + dx_expected = xp[j] - xp[j] * D1 / D0 + xavg = xp[j] + xp_reduced[j] -= self._dynamic_fee(xavg, ys, _fee, feemul) * dx_expected / FEE_DENOMINATOR + + dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1) + + return (dy - 1) / precisions[i] + + +@view +@external +def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: + """ + @notice Calculate the amount received when withdrawing a single coin + @dev Result is the same for underlying or wrapped asset withdrawals + @param _token_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @return Amount of coin received + """ + return self._calc_withdraw_one_coin(_token_amount, i) + + +@external +@nonreentrant('lock') +def remove_liquidity_one_coin( + _token_amount: uint256, + i: int128, + _min_amount: uint256, + _use_underlying: bool = False +) -> uint256: + """ + @notice Withdraw a single coin from the pool + @param _token_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @param _min_amount Minimum amount of coin to receive + @param _use_underlying If True, withdraw underlying assets instead of aTokens + @return Amount of coin received + """ + assert not self.is_killed # dev: is killed + + dy: uint256 = self._calc_withdraw_one_coin(_token_amount, i) + assert dy >= _min_amount, "Not enough coins removed" + + CurveToken(self.lp_token).burnFrom(msg.sender, _token_amount) # dev: insufficient funds + + if _use_underlying: + LendingPool(self.aave_lending_pool).withdraw(self.underlying_coins[i], dy, msg.sender) + else: + assert ERC20(self.coins[i]).transfer(msg.sender, dy) + + log RemoveLiquidityOne(msg.sender, _token_amount, dy) + + return dy + + +### Admin functions ### + +@external +def ramp_A(_future_A: uint256, _future_time: uint256): + assert msg.sender == self.owner # dev: only owner + assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME + assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time + + _initial_A: uint256 = self._A() + _future_A_p: uint256 = _future_A * A_PRECISION + + assert _future_A > 0 and _future_A < MAX_A + if _future_A_p < _initial_A: + assert _future_A_p * MAX_A_CHANGE >= _initial_A + else: + assert _future_A_p <= _initial_A * MAX_A_CHANGE + + self.initial_A = _initial_A + self.future_A = _future_A_p + self.initial_A_time = block.timestamp + self.future_A_time = _future_time + + log RampA(_initial_A, _future_A_p, block.timestamp, _future_time) + + +@external +def stop_ramp_A(): + assert msg.sender == self.owner # dev: only owner + + current_A: uint256 = self._A() + self.initial_A = current_A + self.future_A = current_A + self.initial_A_time = block.timestamp + self.future_A_time = block.timestamp + # now (block.timestamp < t1) is always False, so we return saved A + + log StopRampA(current_A, block.timestamp) + + +@external +def commit_new_fee(new_fee: uint256, new_admin_fee: uint256, new_offpeg_fee_multiplier: uint256): + assert msg.sender == self.owner # dev: only owner + assert self.admin_actions_deadline == 0 # dev: active action + assert new_fee <= MAX_FEE # dev: fee exceeds maximum + assert new_admin_fee <= MAX_ADMIN_FEE # dev: admin fee exceeds maximum + assert new_offpeg_fee_multiplier * new_fee <= MAX_FEE * FEE_DENOMINATOR # dev: offpeg multiplier exceeds maximum + + _deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY + self.admin_actions_deadline = _deadline + self.future_fee = new_fee + self.future_admin_fee = new_admin_fee + self.future_offpeg_fee_multiplier = new_offpeg_fee_multiplier + + log CommitNewFee(_deadline, new_fee, new_admin_fee, new_offpeg_fee_multiplier) + + +@external +def apply_new_fee(): + assert msg.sender == self.owner # dev: only owner + assert block.timestamp >= self.admin_actions_deadline # dev: insufficient time + assert self.admin_actions_deadline != 0 # dev: no active action + + self.admin_actions_deadline = 0 + _fee: uint256 = self.future_fee + _admin_fee: uint256 = self.future_admin_fee + _fml: uint256 = self.future_offpeg_fee_multiplier + self.fee = _fee + self.admin_fee = _admin_fee + self.offpeg_fee_multiplier = _fml + + log NewFee(_fee, _admin_fee, _fml) + + +@external +def revert_new_parameters(): + assert msg.sender == self.owner # dev: only owner + + self.admin_actions_deadline = 0 + + +@external +def commit_transfer_ownership(_owner: address): + assert msg.sender == self.owner # dev: only owner + assert self.transfer_ownership_deadline == 0 # dev: active transfer + + _deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY + self.transfer_ownership_deadline = _deadline + self.future_owner = _owner + + log CommitNewAdmin(_deadline, _owner) + + +@external +def apply_transfer_ownership(): + assert msg.sender == self.owner # dev: only owner + assert block.timestamp >= self.transfer_ownership_deadline # dev: insufficient time + assert self.transfer_ownership_deadline != 0 # dev: no active transfer + + self.transfer_ownership_deadline = 0 + _owner: address = self.future_owner + self.owner = _owner + + log NewAdmin(_owner) + + +@external +def revert_transfer_ownership(): + assert msg.sender == self.owner # dev: only owner + + self.transfer_ownership_deadline = 0 + + +@external +def withdraw_admin_fees(): + assert msg.sender == self.owner # dev: only owner + + for i in range(N_COINS): + value: uint256 = self.admin_balances[i] + if value != 0: + assert ERC20(self.coins[i]).transfer(msg.sender, value) + self.admin_balances[i] = 0 + + +@external +def donate_admin_fees(): + """ + Just in case admin balances somehow become higher than total (rounding error?) + this can be used to fix the state, too + """ + assert msg.sender == self.owner # dev: only owner + self.admin_balances = empty(uint256[N_COINS]) + + +@external +def kill_me(): + assert msg.sender == self.owner # dev: only owner + assert self.kill_deadline > block.timestamp # dev: deadline has passed + self.is_killed = True + + +@external +def unkill_me(): + assert msg.sender == self.owner # dev: only owner + self.is_killed = False + + +@external +def set_aave_referral(referral_code: uint256): + assert msg.sender == self.owner # dev: only owner + assert referral_code < 2 ** 16 # dev: uint16 overflow + self.aave_referral = referral_code \ No newline at end of file diff --git a/protocol/contracts/polygon/curve/strategies/GaugeV2.vy b/protocol/contracts/polygon/curve/strategies/GaugeV2.vy new file mode 100644 index 0000000..c413768 --- /dev/null +++ b/protocol/contracts/polygon/curve/strategies/GaugeV2.vy @@ -0,0 +1,761 @@ +# @version 0.2.8 +""" +@title Liquidity Gauge v2 +@author Curve Finance +@license MIT +""" + +from vyper.interfaces import ERC20 + +implements: ERC20 + + +interface CRV20: + def future_epoch_time_write() -> uint256: nonpayable + def rate() -> uint256: view + +interface Controller: + def period() -> int128: view + def period_write() -> int128: nonpayable + def period_timestamp(p: int128) -> uint256: view + def gauge_relative_weight(addr: address, time: uint256) -> uint256: view + def voting_escrow() -> address: view + def checkpoint(): nonpayable + def checkpoint_gauge(addr: address): nonpayable + +interface Minter: + def token() -> address: view + def controller() -> address: view + def minted(user: address, gauge: address) -> uint256: view + +interface VotingEscrow: + def user_point_epoch(addr: address) -> uint256: view + def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: address + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + + +MAX_REWARDS: constant(uint256) = 8 +TOKENLESS_PRODUCTION: constant(uint256) = 40 +BOOST_WARMUP: constant(uint256) = 2 * 7 * 86400 +WEEK: constant(uint256) = 604800 + +minter: public(address) +crv_token: public(address) +lp_token: public(address) +controller: public(address) +voting_escrow: public(address) +future_epoch_time: public(uint256) + +balanceOf: public(HashMap[address, uint256]) +totalSupply: public(uint256) +allowances: HashMap[address, HashMap[address, uint256]] + +name: public(String[64]) +symbol: public(String[32]) + +# caller -> recipient -> can deposit? +approved_to_deposit: public(HashMap[address, HashMap[address, bool]]) + +working_balances: public(HashMap[address, uint256]) +working_supply: public(uint256) + +# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint +# All values are kept in units of being multiplied by 1e18 +period: public(int128) +period_timestamp: public(uint256[100000000000000000000000000000]) + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint +integrate_inv_supply: public(uint256[100000000000000000000000000000]) # bump epoch when rate() changes + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint +integrate_inv_supply_of: public(HashMap[address, uint256]) +integrate_checkpoint_of: public(HashMap[address, uint256]) + +# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint +# Units: rate * t = already number of coins per address to issue +integrate_fraction: public(HashMap[address, uint256]) + +inflation_rate: public(uint256) + +# For tracking external rewards +reward_contract: public(address) +reward_tokens: public(address[MAX_REWARDS]) + +# deposit / withdraw / claim +reward_sigs: bytes32 + +# reward token -> integral +reward_integral: public(HashMap[address, uint256]) + +# reward token -> claiming address -> integral +reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) + +admin: public(address) +future_admin: public(address) # Can and will be a smart contract +is_killed: public(bool) + + +@external +def __init__( + _name: String[64], + _symbol: String[32], + _lp_token: address, + _minter: address, + _admin: address, +): + """ + @notice Contract constructor + @param _name Token full name + @param _symbol Token symbol + @param _lp_token Liquidity Pool contract address + @param _minter Minter contract address + @param _admin Admin who can kill the gauge + """ + self.name = _name + self.symbol = _symbol + + crv_token: address = Minter(_minter).token() + controller: address = Minter(_minter).controller() + + self.lp_token = _lp_token + self.minter = _minter + self.admin = _admin + self.crv_token = crv_token + self.controller = controller + self.voting_escrow = Controller(controller).voting_escrow() + + self.period_timestamp[0] = block.timestamp + self.inflation_rate = CRV20(crv_token).rate() + self.future_epoch_time = CRV20(crv_token).future_epoch_time_write() + + +@view +@external +def decimals() -> uint256: + """ + @notice Get the number of decimals for this token + @dev Implemented as a view method to reduce gas costs + @return uint256 decimal places + """ + return 18 + + +@view +@external +def integrate_checkpoint() -> uint256: + return self.period_timestamp[self.period] + + +@internal +def _update_liquidity_limit(addr: address, l: uint256, L: uint256): + """ + @notice Calculate limits which depend on the amount of CRV token per-user. + Effectively it calculates working balances to apply amplification + of CRV production by CRV + @param addr User address + @param l User's amount of liquidity (LP tokens) + @param L Total amount of liquidity (LP tokens) + """ + # To be called after totalSupply is updated + _voting_escrow: address = self.voting_escrow + voting_balance: uint256 = ERC20(_voting_escrow).balanceOf(addr) + voting_total: uint256 = ERC20(_voting_escrow).totalSupply() + + lim: uint256 = l * TOKENLESS_PRODUCTION / 100 + if (voting_total > 0) and (block.timestamp > self.period_timestamp[0] + BOOST_WARMUP): + lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 + + lim = min(l, lim) + old_bal: uint256 = self.working_balances[addr] + self.working_balances[addr] = lim + _working_supply: uint256 = self.working_supply + lim - old_bal + self.working_supply = _working_supply + + log UpdateLiquidityLimit(addr, l, L, lim, _working_supply) + + +@internal +def _checkpoint_rewards(_addr: address, _total_supply: uint256): + """ + @notice Claim pending rewards and checkpoint rewards for a user + """ + if _total_supply == 0: + return + + balances: uint256[MAX_REWARDS] = empty(uint256[MAX_REWARDS]) + reward_tokens: address[MAX_REWARDS] = empty(address[MAX_REWARDS]) + for i in range(MAX_REWARDS): + token: address = self.reward_tokens[i] + if token == ZERO_ADDRESS: + break + reward_tokens[i] = token + balances[i] = ERC20(token).balanceOf(self) + + # claim from reward contract + raw_call(self.reward_contract, slice(self.reward_sigs, 8, 4)) # dev: bad claim sig + + for i in range(MAX_REWARDS): + token: address = reward_tokens[i] + if token == ZERO_ADDRESS: + break + dI: uint256 = 10**18 * (ERC20(token).balanceOf(self) - balances[i]) / _total_supply + if _addr == ZERO_ADDRESS: + if dI != 0: + self.reward_integral[token] += dI + continue + + integral: uint256 = self.reward_integral[token] + dI + if dI != 0: + self.reward_integral[token] = integral + + integral_for: uint256 = self.reward_integral_for[token][_addr] + if integral_for < integral: + claimable: uint256 = self.balanceOf[_addr] * (integral - integral_for) / 10**18 + self.reward_integral_for[token][_addr] = integral + response: Bytes[32] = raw_call( + token, + concat( + method_id("transfer(address,uint256)"), + convert(_addr, bytes32), + convert(claimable, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + + +@internal +def _checkpoint(addr: address): + """ + @notice Checkpoint for a user + @param addr User address + """ + _period: int128 = self.period + _period_time: uint256 = self.period_timestamp[_period] + _integrate_inv_supply: uint256 = self.integrate_inv_supply[_period] + rate: uint256 = self.inflation_rate + new_rate: uint256 = rate + prev_future_epoch: uint256 = self.future_epoch_time + if prev_future_epoch >= _period_time: + _token: address = self.crv_token + self.future_epoch_time = CRV20(_token).future_epoch_time_write() + new_rate = CRV20(_token).rate() + self.inflation_rate = new_rate + + if self.is_killed: + # Stop distributing inflation as soon as killed + rate = 0 + + # Update integral of 1/supply + if block.timestamp > _period_time: + _working_supply: uint256 = self.working_supply + _controller: address = self.controller + Controller(_controller).checkpoint_gauge(self) + prev_week_time: uint256 = _period_time + week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp) + + for i in range(500): + dt: uint256 = week_time - prev_week_time + w: uint256 = Controller(_controller).gauge_relative_weight(self, prev_week_time / WEEK * WEEK) + + if _working_supply > 0: + if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time: + # If we went across one or multiple epochs, apply the rate + # of the first epoch until it ends, and then the rate of + # the last epoch. + # If more than one epoch is crossed - the gauge gets less, + # but that'd meen it wasn't called for more than 1 year + _integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply + rate = new_rate + _integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply + else: + _integrate_inv_supply += rate * w * dt / _working_supply + # On precisions of the calculation + # rate ~= 10e18 + # last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) + # _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) + # The largest loss is at dt = 1 + # Loss is 1e-9 - acceptable + + if week_time == block.timestamp: + break + prev_week_time = week_time + week_time = min(week_time + WEEK, block.timestamp) + + _period += 1 + self.period = _period + self.period_timestamp[_period] = block.timestamp + self.integrate_inv_supply[_period] = _integrate_inv_supply + + # Update user-specific integrals + _working_balance: uint256 = self.working_balances[addr] + self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18 + self.integrate_inv_supply_of[addr] = _integrate_inv_supply + self.integrate_checkpoint_of[addr] = block.timestamp + + +@external +def user_checkpoint(addr: address) -> bool: + """ + @notice Record a checkpoint for `addr` + @param addr User address + @return bool success + """ + assert (msg.sender == addr) or (msg.sender == self.minter) # dev: unauthorized + self._checkpoint(addr) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + return True + + +@external +def claimable_tokens(addr: address) -> uint256: + """ + @notice Get the number of claimable tokens per user + @dev This function should be manually changed to "view" in the ABI + @return uint256 number of claimable tokens per user + """ + self._checkpoint(addr) + return self.integrate_fraction[addr] - Minter(self.minter).minted(addr, self) + + +@external +@nonreentrant('lock') +def claimable_reward(_addr: address, _token: address) -> uint256: + """ + @notice Get the number of claimable reward tokens for a user + @dev This function should be manually changed to "view" in the ABI + Calling it via a transaction will claim available reward tokens + @param _addr Account to get reward amount for + @param _token Token to get reward amount for + @return uint256 Claimable reward token amount + """ + claimable: uint256 = ERC20(_token).balanceOf(_addr) + if self.reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_addr, self.totalSupply) + claimable = ERC20(_token).balanceOf(_addr) - claimable + + integral: uint256 = self.reward_integral[_token] + integral_for: uint256 = self.reward_integral_for[_token][_addr] + + if integral_for < integral: + claimable += self.balanceOf[_addr] * (integral - integral_for) / 10**18 + + return claimable + + +@external +@nonreentrant('lock') +def claim_rewards(_addr: address = msg.sender): + """ + @notice Claim available reward tokens for `_addr` + @param _addr Address to claim for + """ + self._checkpoint_rewards(_addr, self.totalSupply) + + +@external +@nonreentrant('lock') +def claim_historic_rewards(_reward_tokens: address[MAX_REWARDS], _addr: address = msg.sender): + """ + @notice Claim reward tokens available from a previously-set staking contract + @param _reward_tokens Array of reward token addresses to claim + @param _addr Address to claim for + """ + for token in _reward_tokens: + if token == ZERO_ADDRESS: + break + integral: uint256 = self.reward_integral[token] + integral_for: uint256 = self.reward_integral_for[token][_addr] + + if integral_for < integral: + claimable: uint256 = self.balanceOf[_addr] * (integral - integral_for) / 10**18 + self.reward_integral_for[token][_addr] = integral + response: Bytes[32] = raw_call( + token, + concat( + method_id("transfer(address,uint256)"), + convert(_addr, bytes32), + convert(claimable, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + + +@external +def kick(addr: address): + """ + @notice Kick `addr` for abusing their boost + @dev Only if either they had another voting event, or their voting escrow lock expired + @param addr Address to kick + """ + _voting_escrow: address = self.voting_escrow + t_last: uint256 = self.integrate_checkpoint_of[addr] + t_ve: uint256 = VotingEscrow(_voting_escrow).user_point_history__ts( + addr, VotingEscrow(_voting_escrow).user_point_epoch(addr) + ) + _balance: uint256 = self.balanceOf[addr] + + assert ERC20(self.voting_escrow).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed + assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed + + self._checkpoint(addr) + self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) + + +@external +def set_approve_deposit(addr: address, can_deposit: bool): + """ + @notice Set whether `addr` can deposit tokens for `msg.sender` + @param addr Address to set approval on + @param can_deposit bool - can this account deposit for `msg.sender`? + """ + self.approved_to_deposit[addr][msg.sender] = can_deposit + + +@external +@nonreentrant('lock') +def deposit(_value: uint256, _addr: address = msg.sender): + """ + @notice Deposit `_value` LP tokens + @dev Depositting also claims pending reward tokens + @param _value Number of tokens to deposit + @param _addr Address to deposit for + """ + if _addr != msg.sender: + assert self.approved_to_deposit[msg.sender][_addr], "Not approved" + + self._checkpoint(_addr) + + if _value != 0: + reward_contract: address = self.reward_contract + total_supply: uint256 = self.totalSupply + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_addr, total_supply) + + total_supply += _value + new_balance: uint256 = self.balanceOf[_addr] + _value + self.balanceOf[_addr] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(_addr, new_balance, total_supply) + + ERC20(self.lp_token).transferFrom(msg.sender, self, _value) + if reward_contract != ZERO_ADDRESS: + deposit_sig: Bytes[4] = slice(self.reward_sigs, 0, 4) + if convert(deposit_sig, uint256) != 0: + raw_call( + reward_contract, + concat(deposit_sig, convert(_value, bytes32)) + ) + + log Deposit(_addr, _value) + log Transfer(ZERO_ADDRESS, _addr, _value) + + +@external +@nonreentrant('lock') +def withdraw(_value: uint256): + """ + @notice Withdraw `_value` LP tokens + @dev Withdrawing also claims pending reward tokens + @param _value Number of tokens to withdraw + """ + self._checkpoint(msg.sender) + + if _value != 0: + reward_contract: address = self.reward_contract + total_supply: uint256 = self.totalSupply + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(msg.sender, total_supply) + + total_supply -= _value + new_balance: uint256 = self.balanceOf[msg.sender] - _value + self.balanceOf[msg.sender] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(msg.sender, new_balance, total_supply) + + if reward_contract != ZERO_ADDRESS: + withdraw_sig: Bytes[4] = slice(self.reward_sigs, 4, 4) + if convert(withdraw_sig, uint256) != 0: + raw_call( + reward_contract, + concat(withdraw_sig, convert(_value, bytes32)) + ) + ERC20(self.lp_token).transfer(msg.sender, _value) + + log Withdraw(msg.sender, _value) + log Transfer(msg.sender, ZERO_ADDRESS, _value) + + +@view +@external +def allowance(_owner : address, _spender : address) -> uint256: + """ + @notice Check the amount of tokens that an owner allowed to a spender + @param _owner The address which owns the funds + @param _spender The address which will spend the funds + @return uint256 Amount of tokens still available for the spender + """ + return self.allowances[_owner][_spender] + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + self._checkpoint(_from) + self._checkpoint(_to) + reward_contract: address = self.reward_contract + + if _value != 0: + total_supply: uint256 = self.totalSupply + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_from, total_supply) + new_balance: uint256 = self.balanceOf[_from] - _value + self.balanceOf[_from] = new_balance + self._update_liquidity_limit(_from, new_balance, total_supply) + + if reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(_to, total_supply) + new_balance = self.balanceOf[_to] + _value + self.balanceOf[_to] = new_balance + self._update_liquidity_limit(_to, new_balance, total_supply) + + log Transfer(_from, _to, _value) + + +@external +@nonreentrant('lock') +def transfer(_to : address, _value : uint256) -> bool: + """ + @notice Transfer token for a specified address + @dev Transferring claims pending reward tokens for the sender and receiver + @param _to The address to transfer to. + @param _value The amount to be transferred. + """ + self._transfer(msg.sender, _to, _value) + + return True + + +@external +@nonreentrant('lock') +def transferFrom(_from : address, _to : address, _value : uint256) -> bool: + """ + @notice Transfer tokens from one address to another. + @dev Transferring claims pending reward tokens for the sender and receiver + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + """ + _allowance: uint256 = self.allowances[_from][msg.sender] + if _allowance != MAX_UINT256: + self.allowances[_from][msg.sender] = _allowance - _value + + self._transfer(_from, _to, _value) + + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @notice Approve the passed address to transfer the specified amount of + tokens on behalf of msg.sender + @dev Beware that changing an allowance via this method brings the risk + that someone may use both the old and new allowance by unfortunate + transaction ordering. This may be mitigated with the use of + {incraseAllowance} and {decreaseAllowance}. + https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will transfer the funds + @param _value The amount of tokens that may be transferred + @return bool success + """ + self.allowances[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + + return True + + +@external +def increaseAllowance(_spender: address, _added_value: uint256) -> bool: + """ + @notice Increase the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _added_value The amount of to increase the allowance + @return bool success + """ + allowance: uint256 = self.allowances[msg.sender][_spender] + _added_value + self.allowances[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + + +@external +def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: + """ + @notice Decrease the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _subtracted_value The amount of to decrease the allowance + @return bool success + """ + allowance: uint256 = self.allowances[msg.sender][_spender] - _subtracted_value + self.allowances[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + + +@external +@nonreentrant('lock') +def set_rewards(_reward_contract: address, _sigs: bytes32, _reward_tokens: address[MAX_REWARDS]): + """ + @notice Set the active reward contract + @dev A reward contract cannot be set while this contract has no deposits + @param _reward_contract Reward contract address. Set to ZERO_ADDRESS to + disable staking. + @param _sigs Four byte selectors for staking, withdrawing and claiming, + right padded with zero bytes. If the reward contract can + be claimed from but does not require staking, the staking + and withdraw selectors should be set to 0x00 + @param _reward_tokens List of claimable tokens for this reward contract + """ + assert msg.sender == self.admin + + lp_token: address = self.lp_token + current_reward_contract: address = self.reward_contract + total_supply: uint256 = self.totalSupply + if current_reward_contract != ZERO_ADDRESS: + self._checkpoint_rewards(ZERO_ADDRESS, total_supply) + withdraw_sig: Bytes[4] = slice(self.reward_sigs, 4, 4) + if convert(withdraw_sig, uint256) != 0: + if total_supply != 0: + raw_call( + current_reward_contract, + concat(withdraw_sig, convert(total_supply, bytes32)) + ) + ERC20(lp_token).approve(current_reward_contract, 0) + + if _reward_contract != ZERO_ADDRESS: + assert _reward_contract.is_contract # dev: not a contract + sigs: bytes32 = _sigs + deposit_sig: Bytes[4] = slice(sigs, 0, 4) + withdraw_sig: Bytes[4] = slice(sigs, 4, 4) + + if convert(deposit_sig, uint256) != 0: + # need a non-zero total supply to verify the sigs + assert total_supply != 0 # dev: zero total supply + ERC20(lp_token).approve(_reward_contract, MAX_UINT256) + + # it would be Very Bad if we get the signatures wrong here, so + # we do a test deposit and withdrawal prior to setting them + raw_call( + _reward_contract, + concat(deposit_sig, convert(total_supply, bytes32)) + ) # dev: failed deposit + assert ERC20(lp_token).balanceOf(self) == 0 + raw_call( + _reward_contract, + concat(withdraw_sig, convert(total_supply, bytes32)) + ) # dev: failed withdraw + assert ERC20(lp_token).balanceOf(self) == total_supply + + # deposit and withdraw are good, time to make the actual deposit + raw_call( + _reward_contract, + concat(deposit_sig, convert(total_supply, bytes32)) + ) + else: + assert convert(withdraw_sig, uint256) == 0 # dev: withdraw without deposit + + self.reward_contract = _reward_contract + self.reward_sigs = _sigs + for i in range(MAX_REWARDS): + if _reward_tokens[i] != ZERO_ADDRESS: + self.reward_tokens[i] = _reward_tokens[i] + elif self.reward_tokens[i] != ZERO_ADDRESS: + self.reward_tokens[i] = ZERO_ADDRESS + else: + assert i != 0 # dev: no reward token + break + + if _reward_contract != ZERO_ADDRESS: + # do an initial checkpoint to verify that claims are working + self._checkpoint_rewards(ZERO_ADDRESS, total_supply) + + +@external +def set_killed(_is_killed: bool): + """ + @notice Set the killed status for this contract + @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV + @param _is_killed Killed status to set + """ + assert msg.sender == self.admin + + self.is_killed = _is_killed + + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of GaugeController to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + log ApplyOwnership(_admin) \ No newline at end of file diff --git a/protocol/contracts/polygon/curve/strategies/StrategyAm3Crv.sol b/protocol/contracts/polygon/curve/strategies/StrategyAm3Crv.sol new file mode 100644 index 0000000..bf3f46b --- /dev/null +++ b/protocol/contracts/polygon/curve/strategies/StrategyAm3Crv.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../../../interfaces/yearn/IController.sol"; +import "../../../../interfaces/curve/Curve.sol"; +import "../../../../interfaces/curve/Gauge.sol"; +import "../../../../interfaces/yearn/IToken.sol"; + +interface IParaswap { + struct MegaSwapPath { + uint256 fromAmountPercent; + Path[] path; + } + + struct Path { + address to; + uint256 totalNetworkFee; //Network fee is associated with 0xv3 trades + Route[] routes; + } + + struct MegaSwapSellData { + address fromToken; + uint256 fromAmount; + uint256 toAmount; + uint256 expectedAmount; + address payable beneficiary; + string referrer; + bool useReduxToken; + MegaSwapPath[] path; + } + + struct Route { + address payable exchange; + address targetExchange; + uint256 percent; + bytes payload; + uint256 networkFee; //Network fee is associated with 0xv3 trades + } + + /** + * @dev The function which performs the mega path swap. + * @param data Data required to perform swap. + */ + function megaSwap(MegaSwapSellData calldata data) + external + payable + returns (uint256); +} + +contract StrategyAm3Crv { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = + address(0xE7a24EF0C5e95Ffb0f6684b813A78F2a3AD7D171); + + address public constant paraswap = + address(0x90249ed4d69D70E709fFCd8beE2c5A566f65dADE); + + address public constant wmatic = + address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); + + address public constant crv = + address(0x172370d5Cd63279eFa6d502DAB29171933a610AF); + + address public constant dai = + address(0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063); + + address public constant pool = + address(0x445FE580eF8d70FF569aB36e80c647af338db351); + + address public constant gauge = + address(0xe381C25de995d62b453aF8B931aAc84fcCaa7A62); + + address public paraswapProxy = + address(0xCD52384e2A96F6E91e4e420de2F9a8C0f1FFB449); + + uint256 public performanceFee = 1500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyAm3Crv"; + } + + function setStrategist(address _strategist) external onlyGovernance { + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function setParaswapProxy(address _paraswapProxy) external onlyGovernance { + paraswapProxy = _paraswapProxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(gauge, _want); + Gauge(gauge).deposit(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(dai != address(_asset), "dai"); + require(wmatic != address(_asset), "wmatic"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal { + Gauge(gauge).withdraw(_amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = balanceOfWant(); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + uint256 _before = balanceOf(); + _withdrawSome(balanceOfPool()); + require(_before == balanceOf(), "!slippage"); + } + + function harvest(bytes memory swapDataWmatic, bytes memory swapDataCrv) + public + { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + + Gauge(gauge).claim_rewards(); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + IERC20(crv).approve(paraswapProxy, _crv); + (bool success, ) = paraswap.call(swapDataCrv); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + + uint256 _wmatic = IERC20(wmatic).balanceOf(address(this)); + if (_wmatic > 0) { + IERC20(wmatic).approve(paraswapProxy, _wmatic); + (bool success, ) = paraswap.call(swapDataWmatic); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + uint256 _dai = IERC20(dai).balanceOf(address(this)); + if (_dai > 0) { + IERC20(dai).approve(pool, _dai); + ICurveFi(pool).add_liquidity([_dai, 0, 0], 0, true); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return Gauge(gauge).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/polygon/curve/strategies/StrategyBtcCurve.sol b/protocol/contracts/polygon/curve/strategies/StrategyBtcCurve.sol new file mode 100644 index 0000000..e43da81 --- /dev/null +++ b/protocol/contracts/polygon/curve/strategies/StrategyBtcCurve.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface ICurveFi { + function add_liquidity( + uint256[2] calldata amounts, + uint256 min_mint_amount, + bool use_underlying + ) external returns (uint256); +} + +interface Gauge { + function deposit(uint256) external; + + function balanceOf(address) external view returns (uint256); + + function withdraw(uint256) external; + + function claim_rewards() external; +} + +contract StrategyBtcCurve { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = + address(0xf8a57c1d3b9629b77b6726a042ca48990A84Fb49); + + address public constant paraswap = + address(0x90249ed4d69D70E709fFCd8beE2c5A566f65dADE); + + address public constant wmatic = + address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); + + address public constant crv = + address(0x172370d5Cd63279eFa6d502DAB29171933a610AF); + + address public constant wbtc = + address(0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6); + + address public constant pool = + address(0xC2d95EEF97Ec6C17551d45e77B590dc1F9117C67); + + address public constant gauge = + address(0xffbACcE0CC7C19d46132f1258FC16CF6871D153c); + + address public paraswapProxy = + address(0xCD52384e2A96F6E91e4e420de2F9a8C0f1FFB449); + + uint256 public performanceFee = 1500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyBtcCurve"; + } + + function setStrategist(address _strategist) external onlyGovernance { + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function setParaswapProxy(address _paraswapProxy) external onlyGovernance { + paraswapProxy = _paraswapProxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(gauge, _want); + Gauge(gauge).deposit(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(wbtc != address(_asset), "wbtc"); + require(wmatic != address(_asset), "wmatic"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal { + Gauge(gauge).withdraw(_amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = balanceOfWant(); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + uint256 _before = balanceOf(); + _withdrawSome(balanceOfPool()); + require(_before == balanceOf(), "!slippage"); + } + + function harvest(bytes memory swapDataWmatic, bytes memory swapDataCrv) + public + { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + + Gauge(gauge).claim_rewards(); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + IERC20(crv).approve(paraswapProxy, _crv); + (bool success, ) = paraswap.call(swapDataCrv); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + + uint256 _wmatic = IERC20(wmatic).balanceOf(address(this)); + if (_wmatic > 0) { + IERC20(wmatic).approve(paraswapProxy, _wmatic); + (bool success, ) = paraswap.call(swapDataWmatic); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + uint256 _wbtc = IERC20(wbtc).balanceOf(address(this)); + if (_wbtc > 0) { + IERC20(wbtc).approve(pool, _wbtc); + ICurveFi(pool).add_liquidity([_wbtc, 0], 0, true); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return Gauge(gauge).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/polygon/curve/strategies/StrategyCurveAm3Crv.sol b/protocol/contracts/polygon/curve/strategies/StrategyCurveAm3Crv.sol new file mode 100644 index 0000000..028e53f --- /dev/null +++ b/protocol/contracts/polygon/curve/strategies/StrategyCurveAm3Crv.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../../../interfaces/yearn/IController.sol"; +import "../../../../interfaces/curve/Curve.sol"; +import "../../../../interfaces/curve/Gauge.sol"; +import "../../../../interfaces/yearn/IToken.sol"; + +interface IParaswap { + + struct MegaSwapPath { + uint256 fromAmountPercent; + Path[] path; + } + + struct Path { + address to; + uint256 totalNetworkFee;//Network fee is associated with 0xv3 trades + Route[] routes; + } + + struct MegaSwapSellData { + address fromToken; + uint256 fromAmount; + uint256 toAmount; + uint256 expectedAmount; + address payable beneficiary; + string referrer; + bool useReduxToken; + MegaSwapPath[] path; + } + + struct Route { + address payable exchange; + address targetExchange; + uint percent; + bytes payload; + uint256 networkFee;//Network fee is associated with 0xv3 trades + } + + /** + * @dev The function which performs the mega path swap. + * @param data Data required to perform swap. + */ + function megaSwap( + MegaSwapSellData calldata data + ) + external + payable + returns (uint256); +} + +contract StrategyCurveAm3Crv { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0xE7a24EF0C5e95Ffb0f6684b813A78F2a3AD7D171 + ); + + address public constant paraswap = address( + 0x90249ed4d69D70E709fFCd8beE2c5A566f65dADE + ); + + address public constant wmatic = address( + 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270 + ); + + address public constant dai = address( + 0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063 + ); + + address public constant pool = address( + 0x445FE580eF8d70FF569aB36e80c647af338db351 + ); + + address public constant gauge = address( + 0xe381C25de995d62b453aF8B931aAc84fcCaa7A62 + ); + + uint256 public performanceFee = 500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCurveAm3Crv"; + } + + function setStrategist(address _strategist) external onlyGovernance { + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(gauge, _want); + Gauge(gauge).deposit(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(dai != address(_asset), "dai"); + require(wmatic != address(_asset), "wmatic"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal { + Gauge(gauge).withdraw(_amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = balanceOfWant(); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + uint256 _before = balanceOf(); + _withdrawSome(balanceOfPool()); + require(_before == balanceOf(), "!slippage"); + } + + function harvest(bytes memory swapData) public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + + Gauge(gauge).claim_rewards(); + + uint256 _wmatic = IERC20(wmatic).balanceOf(address(this)); + if (_wmatic > 0) { + IERC20(wmatic).approve(0xCD52384e2A96F6E91e4e420de2F9a8C0f1FFB449, _wmatic); + (bool success, ) = paraswap.call(swapData); + if (!success) { + // Copy revert reason from call + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } + } + uint256 _dai = IERC20(dai).balanceOf(address(this)); + if (_dai > 0) { + IERC20(dai).approve(pool, _dai); + ICurveFi(pool).add_liquidity([_dai, 0, 0], 0, true); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + earned = earned.add(_want); + emit Harvested(_want, _wmatic); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return Gauge(gauge).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} \ No newline at end of file diff --git a/protocol/contracts/polygon/iron/StrategyIron.sol b/protocol/contracts/polygon/iron/StrategyIron.sol new file mode 100644 index 0000000..9332a52 --- /dev/null +++ b/protocol/contracts/polygon/iron/StrategyIron.sol @@ -0,0 +1,477 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +// yarn add @openzeppelin/contracts@2.5.1 +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +// import "hardhat/console.sol"; + +interface Sushi { + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) + external + returns ( + uint256 amountA, + uint256 amountB, + uint256 liquidity + ); + + function removeLiquidity( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); + + function quote( + uint256 amountA, + uint256 reserveA, + uint256 reserveB + ) external view returns (uint256 amountB); +} + +interface IMasterChef { + struct UserInfo { + uint256 amount; + uint256 rewardDebt; + } + + function deposit(uint256 _pid, uint256 _amount) external; + + function withdraw(uint256 _pid, uint256 _amount) external; + + function userInfo(uint256 pid, address userAddress) + external + view + returns (UserInfo memory); +} + +interface IPair { + function getReserves() + external + view + returns ( + uint256 reserve0, + uint256 reserve1, + uint32 blockTimestampLast + ); + + function totalSupply() external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +contract StrategyIronUsdc { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + //usdc + address public constant want = + address(0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174); + + address public constant iron = + address(0xD86b5923F3AD7b585eD81B448170ae026c65ae9a); + + address public constant titan = + address(0xaAa5B9e6c589642f98a1cDA99B9D024B8407285A); + + address public constant slp = + address(0x85dE135fF062Df790A5f20B79120f17D3da63b2d); + + address public constant masterchef = + address(0x65430393358e55A658BcdE6FF69AB28cF1CbB77a); + + address public constant sushiRouter = + address(0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506); + + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyIron"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function getBestAddLiquidity() internal view returns (uint256, uint256) { + uint256 amUSDC; + uint256 amIRON; + + (uint256 res0, uint256 res1, ) = IPair(slp).getReserves(); + uint256 totalSupply = IPair(slp).totalSupply(); + + uint256 _usdc = IERC20(want).balanceOf(address(this)); // token0 + uint256 _iron = IERC20(iron).balanceOf(address(this)); // token1 + + uint256 lp0 = _usdc.mul(1e12).mul(totalSupply).div(res0.mul(1e12)); // usdc is decimal 6 + uint256 lp1 = _iron.mul(totalSupply).div(res1); + + if (lp0 > lp1) { + amUSDC = Sushi(sushiRouter).quote(_iron, res1, res0); + amIRON = _iron; + } else { + amUSDC = _usdc; + amIRON = Sushi(sushiRouter).quote(_usdc, res0, res1); + } + + return (amUSDC, amIRON); + } + + // return correct amount of usdc to swap based on usdc and iron balance + function getBestSwapAmount() internal view returns (uint256) { + uint256 _usdc = IERC20(want).balanceOf(address(this)); + uint256 _iron = IERC20(iron).balanceOf(address(this)); + (uint256 res0, uint256 res1, ) = IPair(slp).getReserves(); + uint256 ratio = res0.mul(1e12).mul(1e18).div(res1); + uint256 ironInUSDC = _iron.mul(ratio).div(1e18).div(1e12); + + uint256 allUSDC = _usdc.add(ironInUSDC); + uint256 halfUSDC = allUSDC.div(2).mul(ratio).div(1e18); + uint256 expectedIRONinUSDC = allUSDC.sub(halfUSDC); + + if (ironInUSDC >= expectedIRONinUSDC) { + return 0; + } + + return expectedIRONinUSDC.sub(ironInUSDC); + } + + function deposit() public { + uint256 _amount = getBestSwapAmount(); + + if (_amount > 0) { + swapAsset(want, iron, _amount); + } + + uint256 _iron = IERC20(iron).balanceOf(address(this)); + + if (_iron > 0) { + // here we are supposed to have approximately the good usdc iron ratio before calling that + (uint256 amUSDC, uint256 amIRON) = getBestAddLiquidity(); + + IERC20(want).safeApprove(sushiRouter, 0); + IERC20(iron).safeApprove(sushiRouter, 0); + IERC20(iron).safeApprove(sushiRouter, amIRON); + IERC20(want).safeApprove(sushiRouter, amUSDC); + + uint256 _amountIronMin = amIRON.mul(995).div(1000); + uint256 _amountUsdcMin = amUSDC.mul(995).div(1000); + + // console.log("amIRON", amIRON); + // console.log("amUSDC", amUSDC); + // console.log("_amountIronMin", _amountIronMin); + // console.log("_amountUsdcMin", _amountUsdcMin); + + Sushi(sushiRouter).addLiquidity( + iron, + want, + amIRON, + amUSDC, + _amountIronMin, + _amountUsdcMin, + address(this), + now.add(1800) + ); + } + + uint256 _slp = IERC20(slp).balanceOf(address(this)); + + if (_slp > 0) { + IERC20(slp).safeApprove(masterchef, _slp); + IMasterChef(masterchef).deposit(1, _slp); + } + } + + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(iron != address(_asset), "iron"); + require(titan != address(_asset), "titan"); + require(slp != address(_asset), "slp"); + + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + function swapAsset( + address assetFrom, + address assetTo, + uint256 amount + ) internal returns (uint256 output) { + IERC20(assetFrom).safeApprove(sushiRouter, 0); + IERC20(assetFrom).safeApprove(sushiRouter, amount); + + address[] memory path = new address[](2); + path[0] = assetFrom; + path[1] = assetTo; + + uint256[] memory _amounts = + Sushi(sushiRouter).getAmountsOut(amount, path); + uint256 _minimalAmount = _amounts[1].mul(995).div(1000); + + uint256[] memory outputs = + Sushi(sushiRouter).swapExactTokensForTokens( + amount, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + + return outputs[1]; + } + + function swapTitanToWant() internal returns (uint256 output) { + uint256 _titan = IERC20(titan).balanceOf(address(this)); + + if (_titan > 0) { + return swapAsset(titan, want, _titan); + } + } + + function swapIronToWant() internal { + uint256 _iron = IERC20(iron).balanceOf(address(this)); + + if (_iron > 0) { + swapAsset(iron, want, _iron); + } + } + + function swapAssetsToWant() internal { + swapTitanToWant(); + swapIronToWant(); + } + + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _usdc = IERC20(want).balanceOf(address(this)); + + // due to slippage you can have less than desired, especially at launch + if (_amount > _usdc) { + _amount = _usdc; + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal { + uint256 balanceOfPoolInUSDC = balanceOfPool(); + (uint256 res0, uint256 res1, ) = IPair(slp).getReserves(); + uint256 totalSupply = IPair(slp).totalSupply(); + + IMasterChef.UserInfo memory userInfo = + IMasterChef(masterchef).userInfo(1, address(this)); + + uint256 lpAmount = + _amount.mul(userInfo.amount).div(balanceOfPoolInUSDC); + uint256 amountAMin = + lpAmount.mul(res1).div(totalSupply).mul(995).div(1000); + uint256 amountBMin = + lpAmount.mul(res0).div(totalSupply).mul(995).div(1000); + + IMasterChef(masterchef).withdraw(1, lpAmount); + + IERC20(slp).safeApprove(sushiRouter, 0); + IERC20(slp).safeApprove(sushiRouter, lpAmount); + + Sushi(sushiRouter).removeLiquidity( + iron, + want, + lpAmount, + amountAMin, + amountBMin, + address(this), + now.add(1800) + ); + + swapAssetsToWant(); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + (uint256 res0, uint256 res1, ) = IPair(slp).getReserves(); + uint256 totalSupply = IPair(slp).totalSupply(); + + IMasterChef.UserInfo memory userInfo = + IMasterChef(masterchef).userInfo(1, address(this)); + + uint256 lpAmount = userInfo.amount; + uint256 amountAMin = + lpAmount.div(totalSupply).div(res1).mul(995).div(1000); + uint256 amountBMin = + lpAmount.div(totalSupply).div(res0).mul(995).div(1000); + + IMasterChef(masterchef).withdraw(1, lpAmount); + + IERC20(slp).safeApprove(sushiRouter, 0); + IERC20(slp).safeApprove(sushiRouter, lpAmount); + + Sushi(sushiRouter).removeLiquidity( + iron, + want, + lpAmount, + amountAMin, + amountBMin, + address(this), + now.add(1800) + ); + + swapAssetsToWant(); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + + IMasterChef(masterchef).deposit(1, 0); + uint256 output = swapTitanToWant(); + + if (output > 0) { + uint256 _fee = output.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + earned = earned.add(output); + emit Harvested(output, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + (uint256 res0, uint256 res1, ) = IPair(slp).getReserves(); + uint256 totalSupply = IPair(slp).totalSupply(); + + IMasterChef.UserInfo memory userInfo = + IMasterChef(masterchef).userInfo(1, address(this)); + + uint256 _usdc = userInfo.amount.mul(res0).div(totalSupply); + uint256 _iron = userInfo.amount.mul(res1).div(totalSupply); + + uint256 ratio = res0.mul(1e12).mul(1e18).div(res1); + uint256 ironInUSDC = _iron.mul(ratio).div(1e18).div(1e12); + + return _usdc.add(ironInUSDC); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/registries/YRegistry.sol b/protocol/contracts/registries/YRegistry.sol new file mode 100644 index 0000000..a84ede3 --- /dev/null +++ b/protocol/contracts/registries/YRegistry.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/utils/EnumerableSet.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/yearn/IStrategy.sol"; +import "../../interfaces/yearn/IVault.sol"; +import "../../interfaces/yearn/IWrappedVault.sol"; + +contract YRegistry { + using Address for address; + using SafeMath for uint256; + using EnumerableSet for EnumerableSet.AddressSet; + + address public governance; + address public pendingGovernance; + + EnumerableSet.AddressSet private vaults; + EnumerableSet.AddressSet private controllers; + + mapping(address => address) private wrappedVaults; + + mapping(address => bool) public isDelegatedVault; + + constructor(address _governance) public { + require(_governance != address(0), "Missing Governance"); + governance = _governance; + } + + function getName() external pure returns (string memory) { + return "YRegistry"; + } + + function addVault(address _vault) public onlyGovernance { + setVault(_vault); + + (address controller, , , , ) = getVaultData(_vault); + + setController(controller); + } + + function addWrappedVault(address _vault) public onlyGovernance { + setVault(_vault); + address wrappedVault = IWrappedVault(_vault).vault(); + setWrappedVault(_vault, wrappedVault); + + (address controller, , , , ) = getVaultData(_vault); + + // Adds to controllers array + setController(controller); + // TODO Add and track tokens and strategies? [historical] + // (current ones can be obtained via getVaults + getVaultInfo) + } + + function addDelegatedVault(address _vault) public onlyGovernance { + setVault(_vault); + setDelegatedVault(_vault); + + (address controller, , , , ) = getVaultData(_vault); + + // Adds to controllers array + setController(controller); + // TODO Add and track tokens and strategies? [historical] + // (current ones can be obtained via getVaults + getVaultInfo) + } + + function setVault(address _vault) internal { + require(_vault.isContract(), "Vault is not a contract"); + // Checks if vault is already on the array + require(!vaults.contains(_vault), "Vault already exists"); + // Adds unique _vault to vaults array + vaults.add(_vault); + } + + function setWrappedVault(address _vault, address _wrappedVault) internal { + require(_wrappedVault.isContract(), "WrappedVault is not a contract"); + wrappedVaults[_vault] = _wrappedVault; + } + + function setDelegatedVault(address _vault) internal { + // TODO Is there any way to check if a vault is delegated + isDelegatedVault[_vault] = true; + } + + function setController(address _controller) internal { + // Adds Controller to controllers array + if (!controllers.contains(_controller)) { + controllers.add(_controller); + } + } + + function getVaultData(address _vault) + internal + view + returns ( + address controller, + address token, + address strategy, + bool isWrapped, + bool isDelegated + ) + { + address vault = _vault; + isWrapped = wrappedVaults[_vault] != address(0); + if (isWrapped) { + vault = wrappedVaults[_vault]; + } + isDelegated = isDelegatedVault[vault]; + + // Get values from controller + controller = IVault(vault).controller(); + if (isWrapped && IVault(vault).underlying() != address(0)) { + token = IVault(_vault).token(); // Use non-wrapped vault + } else { + token = IVault(vault).token(); + } + + if (isDelegated) { + strategy = IController(controller).strategies(vault); + } else { + strategy = IController(controller).strategies(token); + } + + // Check if vault is set on controller for token + address controllerVault = address(0); + if (isDelegated) { + controllerVault = IController(controller).vaults(strategy); + } else { + controllerVault = IController(controller).vaults(token); + } + require(controllerVault == vault, "Controller vault address does not match"); // Might happen on Proxy Vaults + + // Check if strategy has the same token as vault + if (isWrapped) { + address underlying = IVault(vault).underlying(); + require(underlying == token, "WrappedVault token address does not match"); // Might happen? + } else if (!isDelegated) { + address strategyToken = IStrategy(strategy).want(); + require(token == strategyToken, "Strategy token address does not match"); // Might happen? + } + + return (controller, token, strategy, isWrapped, isDelegated); + } + + // Vaults getters + function getVault(uint256 index) external view returns (address vault) { + return vaults.get(index); + } + + function getVaultsLength() external view returns (uint256) { + return vaults.length(); + } + + function getVaults() external view returns (address[] memory) { + address[] memory vaultsArray = new address[](vaults.length()); + for (uint256 i = 0; i < vaults.length(); i++) { + vaultsArray[i] = vaults.get(i); + } + return vaultsArray; + } + + function getVaultInfo(address _vault) + external + view + returns ( + address controller, + address token, + address strategy, + bool isWrapped, + bool isDelegated + ) + { + (controller, token, strategy, isWrapped, isDelegated) = getVaultData(_vault); + return (controller, token, strategy, isWrapped, isDelegated); + } + + function getVaultsInfo() + external + view + returns ( + address[] memory controllerArray, + address[] memory tokenArray, + address[] memory strategyArray, + bool[] memory isWrappedArray, + bool[] memory isDelegatedArray + ) + { + controllerArray = new address[](vaults.length()); + tokenArray = new address[](vaults.length()); + strategyArray = new address[](vaults.length()); + isWrappedArray = new bool[](vaults.length()); + isDelegatedArray = new bool[](vaults.length()); + + for (uint256 i = 0; i < vaults.length(); i++) { + (address _controller, address _token, address _strategy, bool _isWrapped, bool _isDelegated) = getVaultData(vaults.get(i)); + controllerArray[i] = _controller; + tokenArray[i] = _token; + strategyArray[i] = _strategy; + isWrappedArray[i] = _isWrapped; + isDelegatedArray[i] = _isDelegated; + } + } + + // Governance setters + function setPendingGovernance(address _pendingGovernance) external onlyGovernance { + pendingGovernance = _pendingGovernance; + } + + function acceptGovernance() external onlyPendingGovernance { + governance = msg.sender; + } + + modifier onlyGovernance { + require(msg.sender == governance, "Only governance can call this function."); + _; + } + modifier onlyPendingGovernance { + require(msg.sender == pendingGovernance, "Only pendingGovernance can call this function."); + _; + } +} diff --git a/protocol/contracts/sdt-claim-distribution/MerkleDistributorSdt.sol b/protocol/contracts/sdt-claim-distribution/MerkleDistributorSdt.sol new file mode 100644 index 0000000..b065ca2 --- /dev/null +++ b/protocol/contracts/sdt-claim-distribution/MerkleDistributorSdt.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.7.6; + +interface IERC20 { + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) + external + returns (bool); +} + +// MerkleDistributor for distributing SDT to veCRV holders +// Based on the EMN refund contract by banteg - https://github.com/banteg/your-eminence +contract MerkleDistributorSdt { + bytes32[] public merkleRoots; + bytes32 public pendingMerkleRoot; + uint256 public lastRoot; + + // reward token + address + public constant rewardToken = 0x73968b9a57c6E53d41345FD57a6E6ae27d6CDB2F; + // admin address which can propose adding a new merkle root + address public proposalAuthority; + // admin address which approves or rejects a proposed merkle root + address public reviewAuthority; + + event Claimed( + uint256 merkleIndex, + uint256 index, + address account, + uint256 amount + ); + + // This is a packed array of booleans. + mapping(uint256 => mapping(uint256 => uint256)) private claimedBitMap; + + constructor(address _proposalAuthority, address _reviewAuthority) public { + proposalAuthority = _proposalAuthority; + reviewAuthority = _reviewAuthority; + } + + function setProposalAuthority(address _account) public { + require(msg.sender == proposalAuthority); + proposalAuthority = _account; + } + + function setReviewAuthority(address _account) public { + require(msg.sender == reviewAuthority); + reviewAuthority = _account; + } + + // Each week, the proposal authority calls to submit the merkle root for a new airdrop. + function proposeMerkleRoot(bytes32 _merkleRoot) public { + require(msg.sender == proposalAuthority); + require(pendingMerkleRoot == 0x00); + require(block.timestamp > lastRoot + 604800); + pendingMerkleRoot = _merkleRoot; + } + + // After validating the correctness of the pending merkle root, the reviewing authority + // calls to confirm it and the distribution may begin. + function reviewPendingMerkleRoot(bool _approved) public { + require(msg.sender == reviewAuthority); + require(pendingMerkleRoot != 0x00); + if (_approved) { + merkleRoots.push(pendingMerkleRoot); + lastRoot = block.timestamp; + } + delete pendingMerkleRoot; + } + + function isClaimed(uint256 merkleIndex, uint256 index) + public + view + returns (bool) + { + uint256 claimedWordIndex = index / 256; + uint256 claimedBitIndex = index % 256; + uint256 claimedWord = claimedBitMap[merkleIndex][claimedWordIndex]; + uint256 mask = (1 << claimedBitIndex); + return claimedWord & mask == mask; + } + + function _setClaimed(uint256 merkleIndex, uint256 index) private { + uint256 claimedWordIndex = index / 256; + uint256 claimedBitIndex = index % 256; + claimedBitMap[merkleIndex][claimedWordIndex] = + claimedBitMap[merkleIndex][claimedWordIndex] | + (1 << claimedBitIndex); + } + + function claim( + uint256 merkleIndex, + uint256 index, + uint256 amount, + bytes32[] calldata merkleProof + ) external { + require( + merkleIndex < merkleRoots.length, + "MerkleDistributor: Invalid merkleIndex" + ); + require( + !isClaimed(merkleIndex, index), + "MerkleDistributor: Drop already claimed." + ); + + // Verify the merkle proof. + bytes32 node = keccak256(abi.encodePacked(index, msg.sender, amount)); + require( + verify(merkleProof, merkleRoots[merkleIndex], node), + "MerkleDistributor: Invalid proof." + ); + + // Mark it claimed and send the token. + _setClaimed(merkleIndex, index); + IERC20(rewardToken).transfer(msg.sender, amount); + + emit Claimed(merkleIndex, index, msg.sender, amount); + } + + function verify( + bytes32[] memory proof, + bytes32 root, + bytes32 leaf + ) internal pure returns (bool) { + bytes32 computedHash = leaf; + + for (uint256 i = 0; i < proof.length; i++) { + bytes32 proofElement = proof[i]; + + if (computedHash <= proofElement) { + // Hash(current computed hash + current element of the proof) + computedHash = keccak256( + abi.encodePacked(computedHash, proofElement) + ); + } else { + // Hash(current element of the proof + current computed hash) + computedHash = keccak256( + abi.encodePacked(proofElement, computedHash) + ); + } + } + + // Check if the computed hash (root) is equal to the provided root + return computedHash == root; + } +} diff --git a/protocol/contracts/sdt/SDT.sol b/protocol/contracts/sdt/SDT.sol new file mode 100644 index 0000000..3ce748c --- /dev/null +++ b/protocol/contracts/sdt/SDT.sol @@ -0,0 +1,16 @@ +/** + *Submitted for verification at Etherscan.io on 2020-07-17 + */ + +pragma solidity 0.6.7; + +import "../temp/openzeppelin/ERC20.sol"; +import "../temp/openzeppelin/Ownable.sol"; + +// StakeDaoToken with Governance. +contract SDT is ERC20("Stake DAO Token", "SDT"), Ownable { + /// @notice Creates `_amount` token to `_to`. Must only be called by the owner (MasterChef). + function mint(address _to, uint256 _amount) public onlyOwner { + _mint(_to, _amount); + } +} diff --git a/protocol/contracts/staking/Sanctuary.sol b/protocol/contracts/staking/Sanctuary.sol new file mode 100644 index 0000000..787bb7e --- /dev/null +++ b/protocol/contracts/staking/Sanctuary.sol @@ -0,0 +1,74 @@ +//SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.6.0; + +import "../temp/openzeppelin/IERC20.sol"; +import "../temp/openzeppelin/Context.sol"; +import "../temp/openzeppelin/SafeMath.sol"; +import "../temp/openzeppelin/Address.sol"; +import "../temp/openzeppelin/ERC20.sol"; +import "../temp/openzeppelin/Ownable.sol"; + +contract Sanctuary is ERC20("Staked SDT", "xSDT"), Ownable { + using SafeMath for uint256; + IERC20 public sdt; + address public rewardDistribution; + + event Stake(address indexed staker, uint256 xsdtReceived); + event Unstake(address indexed unstaker, uint256 sdtReceived); + event RewardDistributorSet(address indexed newRewardDistributor); + event SdtFeeReceived(address indexed from, uint256 sdtAmount); + + modifier onlyRewardDistribution() { + require( + _msgSender() == rewardDistribution, + "Caller is not reward distribution" + ); + _; + } + + constructor(IERC20 _sdt) public { + sdt = _sdt; + } + + // Enter the Sanctuary. Pay some SDTs. Earn some shares. + function enter(uint256 _amount) public { + uint256 totalSdt = sdt.balanceOf(address(this)); + uint256 totalShares = totalSupply(); + if (totalShares == 0 || totalSdt == 0) { + _mint(_msgSender(), _amount); + emit Stake(_msgSender(), _amount); + } else { + uint256 what = _amount.mul(totalShares).div(totalSdt); + _mint(_msgSender(), what); + emit Stake(_msgSender(), what); + } + sdt.transferFrom(_msgSender(), address(this), _amount); + } + + // Leave the Sanctuary. Claim back your SDTs. + function leave(uint256 _share) public { + uint256 totalShares = totalSupply(); + uint256 what = + _share.mul(sdt.balanceOf(address(this))).div(totalShares); + _burn(_msgSender(), _share); + sdt.transfer(_msgSender(), what); + emit Unstake(_msgSender(), what); + } + + function setRewardDistribution(address _rewardDistribution) + external + onlyOwner + { + rewardDistribution = _rewardDistribution; + emit RewardDistributorSet(_rewardDistribution); + } + + function notifyRewardAmount(uint256 _balance) + external + onlyRewardDistribution + { + sdt.transferFrom(_msgSender(), address(this), _balance); + emit SdtFeeReceived(_msgSender(), _balance); + } +} diff --git a/protocol/contracts/strategies/CurveYCRVVoter.sol b/protocol/contracts/strategies/CurveYCRVVoter.sol new file mode 100644 index 0000000..84f5a5b --- /dev/null +++ b/protocol/contracts/strategies/CurveYCRVVoter.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/curve/Curve.sol"; +import "../../interfaces/curve/Gauge.sol"; +import "../../interfaces/curve/Mintr.sol"; +import "../../interfaces/curve/VoteEscrow.sol"; +import "../../interfaces/uniswap/Uni.sol"; +import "../../interfaces/yearn/IToken.sol"; + +contract CurveYCRVVoter { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address(0xdF5e0e81Dff6FAF3A7e52BA697820c5e32D806A8); + address public constant pool = address(0xFA712EE4788C042e2B7BB55E6cb8ec569C4530c1); + address public constant mintr = address(0xd061D61a4d941c39E5453435B6345Dc261C2fcE0); + address public constant crv = address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant escrow = address(0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2); + + address public governance; + address public strategy; + + constructor() public { + governance = msg.sender; + } + + function getName() external pure returns (string memory) { + return "CurveYCRVVoter"; + } + + function setStrategy(address _strategy) external { + require(msg.sender == governance, "!governance"); + strategy = _strategy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeApprove(pool, 0); + IERC20(want).safeApprove(pool, _want); + Gauge(pool).deposit(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == strategy, "!controller"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(strategy, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == strategy, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + IERC20(want).safeTransfer(strategy, _amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == strategy, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + IERC20(want).safeTransfer(strategy, balance); + } + + function _withdrawAll() internal { + Gauge(pool).withdraw(Gauge(pool).balanceOf(address(this))); + } + + function createLock(uint256 _value, uint256 _unlockTime) external { + require(msg.sender == strategy || msg.sender == governance, "!authorized"); + IERC20(crv).safeApprove(escrow, 0); + IERC20(crv).safeApprove(escrow, _value); + VoteEscrow(escrow).create_lock(_value, _unlockTime); + } + + function increaseAmount(uint256 _value) external { + require(msg.sender == strategy || msg.sender == governance, "!authorized"); + IERC20(crv).safeApprove(escrow, 0); + IERC20(crv).safeApprove(escrow, _value); + VoteEscrow(escrow).increase_amount(_value); + } + + function release() external { + require(msg.sender == strategy || msg.sender == governance, "!authorized"); + VoteEscrow(escrow).withdraw(); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + Gauge(pool).withdraw(_amount); + return _amount; + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return Gauge(pool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function execute( + address to, + uint256 value, + bytes calldata data + ) external returns (bool, bytes memory) { + require(msg.sender == strategy || msg.sender == governance, "!governance"); + (bool success, bytes memory result) = to.call.value(value)(data); + + return (success, result); + } +} diff --git a/protocol/contracts/strategies/StrategyAaveMakerCurveProxy.sol b/protocol/contracts/strategies/StrategyAaveMakerCurveProxy.sol new file mode 100644 index 0000000..e69de29 diff --git a/protocol/contracts/strategies/StrategyAaveUSDCLeverage.sol b/protocol/contracts/strategies/StrategyAaveUSDCLeverage.sol new file mode 100644 index 0000000..f9ecc1a --- /dev/null +++ b/protocol/contracts/strategies/StrategyAaveUSDCLeverage.sol @@ -0,0 +1,440 @@ +pragma solidity ^0.5.17; + +pragma experimental ABIEncoderV2; + +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/dydx/DydxFlashloanBase.sol"; +import "../../interfaces/dydx/ICallee.sol"; +import "../../interfaces/uniswap/Uni.sol"; + +interface ILendingPool { + function deposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external; + + function withdraw( + address asset, + uint256 amount, + address to + ) external; + + function borrow( + address asset, + uint256 amount, + uint256 interestRateMode, + uint16 referralCode, + address onBehalfOf + ) external; + + function repay( + address asset, + uint256 amount, + uint256 rateMode, + address onBehalfOf + ) external; +} + +interface IStableDebtToken { + function principalBalanceOf(address user) external view returns (uint256); +} + +interface IVariableDebtToken { + function balanceOf(address user) external view returns (uint256); +} + +interface IAToken { + function balanceOf(address user) external view returns (uint256); + + function scaledBalanceOf(address user) external view returns (uint256); + + function getScaledUserBalanceAndSupply(address user) + external + view + returns (uint256, uint256); +} + +interface IStakedTokenIncentivesController { + function getRewardsBalance(address[] calldata assets, address user) + external + view + returns (uint256); + + function claimRewards( + address[] calldata assets, + uint256 amount, + address to + ) external returns (uint256); +} + +interface IStakedToken { + function cooldown() external; + + function redeem(address to, uint256 amount) external; +} + +contract StrategyAaveUSDCLeverage is DydxFlashloanBase, ICallee { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + ); + + address public constant aave = address( + 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9 + ); + + address public constant weth = address( + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + ); + + address public constant uni = address( + 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D + ); + + address public constant treasury = address( + 0x9D75C85f864Ab9149E23F27C35addaE09B9B909C + ); + + address public constant aaveLendingPool = address( + 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9 + ); + + address public constant aUSDC = address( + 0xBcca60bB61934080951369a648Fb03DF4F96263C + ); + + address public constant stableDebtUSDC = address( + 0xE4922afAB0BbaDd8ab2a88E0C79d884Ad337fcA6 + ); + + address public constant variableDebtUSDC = address( + 0x619beb58998eD2278e08620f97007e1116D5D25b + ); + + address public constant stkAAVE = address( + 0x4da27a545c0c5B758a6BA100e3a049001de870f5 + ); + + address public constant stkAAVEController = address( + 0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5 + ); + + address private constant SOLO = address( + 0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e + ); + + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + uint256 public flashLoanleverage = 3; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + constructor(address _governance, address _controller) public { + governance = _governance; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyAaveUSDCLeverage"; + } + + // Returns the current lending position of the strategy in AAVE + function getCurrentPosition() + public + view + returns (uint256 deposits, uint256 borrows) + { + deposits = IAToken(aUSDC).balanceOf(address(this)); + borrows = IVariableDebtToken(variableDebtUSDC).balanceOf(address(this)); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + (uint256 deposits, uint256 borrows) = getCurrentPosition(); + return deposits.sub(borrows); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function maxApprove() external { + require(msg.sender == governance, "!governance"); + IERC20(aave).safeApprove(uni, uint256(-1)); + IERC20(want).safeApprove(SOLO, uint256(-1)); + IERC20(want).safeApprove(aaveLendingPool, uint256(-1)); + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + function setFlashLoanLeverage(uint256 _leverage) external { + require(msg.sender == governance, "!governance"); + flashLoanleverage = _leverage; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + // _want = _want.sub(1e6); + if (_want > 0) { + // depositing n + ILendingPool(aaveLendingPool).deposit( + want, + _want, + address(this), + 0 + ); + } + // 4x leverage on deposit amount + doDyDxFlashLoan(false, _want.mul(flashLoanleverage), 0); + } + + function doDyDxFlashLoan( + bool deficit, + uint256 amountDesired, + uint256 ask + ) private returns (uint256) { + uint256 amount = amountDesired; + ISoloMargin solo = ISoloMargin(SOLO); + uint256 marketId = _getMarketIdFromTokenAddress(SOLO, address(want)); + // Not enough USDC in DyDx. So we take all we can + uint256 amountInSolo = IERC20(want).balanceOf(SOLO); + if (amountInSolo < amount) { + amount = amountInSolo; + } + uint256 repayAmount = _getRepaymentAmountInternal(amount); + bytes memory data = abi.encode(deficit, amount, repayAmount, ask); + // 1. Withdraw $ + // 2. Call callFunction(...) + // 3. Deposit back $ + Actions.ActionArgs[] memory operations = new Actions.ActionArgs[](3); + operations[0] = _getWithdrawAction(marketId, amount); + operations[1] = _getCallAction( + // Encode custom data for callFunction + data + ); + operations[2] = _getDepositAction(marketId, repayAmount); + Account.Info[] memory accountInfos = new Account.Info[](1); + accountInfos[0] = _getAccountInfo(); + solo.operate(accountInfos, operations); + return amount; + } + + function callFunction( + address sender, + Account.Info memory account, + bytes memory data + ) public { + (bool deficit, uint256 amount, uint256 repayAmount, uint256 ask) = abi + .decode(data, (bool, uint256, uint256, uint256)); + + _loanLogic(deficit, amount, repayAmount, ask); + } + + function _loanLogic( + bool deficit, + uint256 amount, + uint256 repayAmount, + uint256 ask + ) internal { + uint256 bal = IERC20(want).balanceOf(address(this)); + require(bal >= amount, "FLASH_FAILED"); // to stop malicious calls + // in deficit we repay amount and then withdraw + if (deficit) { + // repaying yn + uint256 poolBalance = balanceOfPool(); + ILendingPool(aaveLendingPool).repay(want, amount, 2, address(this)); + // need to read balanceOfPool() before repay() + if (ask == poolBalance) { + ILendingPool(aaveLendingPool).withdraw( + want, + uint256(-1), + address(this) + ); + } else { + // withdrawing yn + n + ILendingPool(aaveLendingPool).withdraw( + want, + amount.add(amount.div(flashLoanleverage)).add(2), + address(this) + ); + } + } else { + // depositing yn + ILendingPool(aaveLendingPool).deposit( + want, + amount, + address(this), + 0 + ); + // borrowing yn + 2 + // fee is so low for dydx that it does not effect our liquidation risk. + // DONT USE FOR AAVE FLASH LOAN + ILendingPool(aaveLendingPool).borrow( + want, + repayAmount, + 2, + 0, + address(this) + ); + } + } + + function claimStkAAVE() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + + address[] memory assets = new address[](2); + assets[0] = aUSDC; + assets[1] = variableDebtUSDC; + + uint256 rewards = IStakedTokenIncentivesController(stkAAVEController) + .getRewardsBalance(assets, address(this)); + // claim stkAAVE + IStakedTokenIncentivesController(stkAAVEController).claimRewards( + assets, + rewards, + address(this) + ); + // activate 10 day cooldown + IStakedToken(stkAAVE).cooldown(); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + // burn stkAAVE for AAVE + IStakedToken(stkAAVE).redeem(address(this), uint256(-1)); + + uint256 _aave = IERC20(aave).balanceOf(address(this)); + address[] memory path = new address[](3); + path[0] = aave; + path[1] = weth; + path[2] = want; + + Uni(uni).swapExactTokensForTokens( + _aave, + uint256(0), + path, + address(this), + now.add(1800) + ); + + uint256 _want = IERC20(want).balanceOf(address(this)); + uint256 perfFees = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(treasury, perfFees); + + earned = earned.add(balanceOfWant()); + emit Harvested(balanceOfWant(), earned); + + deposit(); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + require( + _amount <= balanceOfPool(), + "Withdraw amount exceeds pool balance" + ); + (uint256 deposits, uint256 borrows) = getCurrentPosition(); + + if (_amount == balanceOfPool()) { + doDyDxFlashLoan(true, borrows, _amount); + } else { + doDyDxFlashLoan(true, _amount.mul(flashLoanleverage), 0); + } + return balanceOfWant(); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + uint256 diff; + + uint256 usdcBefore = IERC20(want).balanceOf(address(this)); + _withdrawSome(_amount.sub(_balance)); + uint256 usdcAfter = IERC20(want).balanceOf(address(this)); + + diff = usdcAfter.sub(usdcBefore); + if (diff < _amount.sub(_balance)) { + _amount = _balance.add(diff); + } + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + // Exit entire position, in case c-ratio is in danger. Call withdrawAll() on controller + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawSome(balanceOfPool()); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } +} diff --git a/protocol/contracts/strategies/StrategyBunchyDev.sol b/protocol/contracts/strategies/StrategyBunchyDev.sol new file mode 100644 index 0000000..269fc24 --- /dev/null +++ b/protocol/contracts/strategies/StrategyBunchyDev.sol @@ -0,0 +1,186 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; +import "../../interfaces/yearn/IController.sol"; + +interface IDev { + function deposit(address _to, uint256 _amount) external returns (bool); +} + +interface ILockup { + function withdraw(address _property, uint256 _amount) external; + + function getValue(address _property, address _sender) + external + view + returns (uint256); +} + +interface IAddressConfig { + function lockup() external view returns (address); +} + +contract StrategyBunchyDev { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0x5cAf454Ba92e6F2c929DF14667Ee360eD9fD5b26 + ); + + address public constant property = address( + 0x28Ea94a44D5a85cAa13016d4eEFdfe5A669A2994 + ); + + address public constant addressConfig = address( + 0x1D415aa39D647834786EB9B5a333A50e9935b796 + ); + + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + constructor(address _governance, address _controller) public { + governance = _governance; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyBunchyDev"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IDev(want).deposit(property, _want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + + deposit(); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + lockup().withdraw(property, _amount); + return balanceOfWant(); + } + + function withdrawToVault(uint256 amount) external { + require(msg.sender == governance, "!governance"); + amount = _withdrawSome(amount); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function lockup() public view returns (ILockup) { + return ILockup(IAddressConfig(addressConfig).lockup()); + } + + function _withdrawAll() internal { + lockup().withdraw(property, lockup().getValue(property, address(this))); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + + lockup().withdraw(property, 0); + + earned = earned.add(balanceOfWant()); + emit Harvested(balanceOfWant(), earned); + + deposit(); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return lockup().getValue(property, address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyCreamYFI.sol b/protocol/contracts/strategies/StrategyCreamYFI.sol new file mode 100644 index 0000000..c540e0e --- /dev/null +++ b/protocol/contracts/strategies/StrategyCreamYFI.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/cream/Controller.sol"; +import "../../interfaces/compound/Token.sol"; +import "../../interfaces/uniswap/Uni.sol"; + +import "../../interfaces/yearn/IController.sol"; + +contract StrategyCreamYFI { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address(0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e); + + Creamtroller public constant creamtroller = Creamtroller(0x3d5BC3c8d13dcB8bF317092d84783c2697AE9258); + + address public constant crYFI = address(0xCbaE0A83f4f9926997c8339545fb8eE32eDc6b76); + address public constant cream = address(0x2ba592F78dB6436527729929AAf6c908497cB200); + + address public constant uni = address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + address public constant weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // used for cream <> weth <> yfi route + + uint256 public performanceFee = 500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCreamYFI"; + } + + function setStrategist(address _strategist) external { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeApprove(crYFI, 0); + IERC20(want).safeApprove(crYFI, _want); + cToken(crYFI).mint(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(crYFI != address(_asset), "crYFI"); + require(cream != address(_asset), "cream"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + uint256 amount = balanceC(); + if (amount > 0) { + _withdrawSome(balanceCInToken().sub(1)); + } + } + + function harvest() public { + require(msg.sender == strategist || msg.sender == governance, "!authorized"); + Creamtroller(creamtroller).claimComp(address(this)); + uint256 _cream = IERC20(cream).balanceOf(address(this)); + if (_cream > 0) { + IERC20(cream).safeApprove(uni, 0); + IERC20(cream).safeApprove(uni, _cream); + + address[] memory path = new address[](3); + path[0] = cream; + path[1] = weth; + path[2] = want; + + Uni(uni).swapExactTokensForTokens(_cream, uint256(0), path, address(this), now.add(1800)); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 b = balanceC(); + uint256 bT = balanceCInToken(); + // can have unintentional rounding errors + uint256 amount = (b.mul(_amount)).div(bT).add(1); + uint256 _before = IERC20(want).balanceOf(address(this)); + _withdrawC(amount); + uint256 _after = IERC20(want).balanceOf(address(this)); + uint256 _withdrew = _after.sub(_before); + return _withdrew; + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function _withdrawC(uint256 amount) internal { + cToken(crYFI).redeem(amount); + } + + function balanceCInToken() public view returns (uint256) { + // Mantisa 1e18 to decimals + uint256 b = balanceC(); + if (b > 0) { + b = b.mul(cToken(crYFI).exchangeRateStored()).div(1e18); + } + return b; + } + + function balanceC() public view returns (uint256) { + return IERC20(crYFI).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceCInToken()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyCurve3CrvVoterProxy.sol b/protocol/contracts/strategies/StrategyCurve3CrvVoterProxy.sol new file mode 100644 index 0000000..d4ae051 --- /dev/null +++ b/protocol/contracts/strategies/StrategyCurve3CrvVoterProxy.sol @@ -0,0 +1,241 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/curve/Gauge.sol"; +import "../../interfaces/curve/Mintr.sol"; +import "../../interfaces/uniswap/Uni.sol"; +import "../../interfaces/curve/Curve.sol"; +import "../../interfaces/yearn/IToken.sol"; +import "../../interfaces/yearn/IVoterProxy.sol"; + +contract StrategyCurve3CrvVoterProxy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490 + ); + address public constant crv = address( + 0xD533a949740bb3306d119CC777fa900bA034cd52 + ); + address public constant uni = address( + 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D + ); + address public constant weth = address( + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + ); // used for crv <> weth <> dai route + + address public constant dai = address( + 0x6B175474E89094C44Da98b954EedeAC495271d0F + ); + address public constant curve = address( + 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7 + ); + + address public constant gauge = address( + 0xbFcF63294aD7105dEa65aA58F8AE5BE2D9d0952A + ); + //address public constant voter = address(0xF147b8125d2ef93FB6965Db97D6746952a133934); + address public constant voter = address( + 0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6 + ); + + uint256 public keepCRV = 1000; + uint256 public performanceFee = 1500; + uint256 public strategistReward = 0; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCurve3CrvVoterProxy"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function setStrategistReward(uint256 _strategistReward) + external + onlyGovernance + { + strategistReward = _strategistReward; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeTransfer(proxy, _want); + IVoterProxy(proxy).deposit(gauge, want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(crv != address(_asset), "crv"); + require(dai != address(_asset), "dai"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + return IVoterProxy(proxy).withdraw(gauge, want, _amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IVoterProxy(proxy).withdrawAll(gauge, want); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + IVoterProxy(proxy).harvest(gauge, false); + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(uni, 0); + IERC20(crv).safeApprove(uni, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = dai; + + Uni(uni).swapExactTokensForTokens( + _crv, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + uint256 _dai = IERC20(dai).balanceOf(address(this)); + if (_dai > 0) { + IERC20(dai).safeApprove(curve, 0); + IERC20(dai).safeApprove(curve, _dai); + ICurveFi(curve).add_liquidity([_dai, 0, 0], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + uint256 _reward = _want.mul(strategistReward).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + IERC20(want).safeTransfer(strategist, _reward); + deposit(); + } + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IVoterProxy(proxy).balanceOf(gauge); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyCurveBTCVoterProxy.sol b/protocol/contracts/strategies/StrategyCurveBTCVoterProxy.sol new file mode 100644 index 0000000..93fff61 --- /dev/null +++ b/protocol/contracts/strategies/StrategyCurveBTCVoterProxy.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/curve/Gauge.sol"; +import "../../interfaces/curve/Mintr.sol"; +import "../../interfaces/uniswap/Uni.sol"; +import "../../interfaces/curve/Curve.sol"; +import "../../interfaces/yearn/IToken.sol"; +import "../../interfaces/yearn/IVoterProxy.sol"; + +contract StrategyCurveBTCVoterProxy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0x075b1bb99792c9E1041bA13afEf80C91a1e70fB3 + ); + address public constant crv = address( + 0xD533a949740bb3306d119CC777fa900bA034cd52 + ); + address public constant uni = address( + 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D + ); + address public constant weth = address( + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + ); // used for crv <> weth <> wbtc route + + address public constant wbtc = address( + 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 + ); + address public constant curve = address( + 0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714 + ); + + address public constant gauge = address( + 0x705350c4BcD35c9441419DdD5d2f097d7a55410F + ); + //address public constant voter = address(0xF147b8125d2ef93FB6965Db97D6746952a133934); + address public constant voter = address( + 0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6 + ); + + uint256 public keepCRV = 1000; + uint256 public constant keepCRVMax = 10000; + + uint256 public performanceFee = 1500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCurveBTCVoterProxy"; + } + + function setStrategist(address _strategist) external onlyGovernance { + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeTransfer(proxy, _want); + IVoterProxy(proxy).deposit(gauge, want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(crv != address(_asset), "crv"); + require(wbtc != address(_asset), "wbtc"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + uint256 _before = balanceOf(); + IVoterProxy(proxy).withdrawAll(gauge, want); + require(_before == balanceOf(), "!slippage"); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + IVoterProxy(proxy).harvest(gauge, false); + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(keepCRVMax); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(uni, 0); + IERC20(crv).safeApprove(uni, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = wbtc; + + Uni(uni).swapExactTokensForTokens( + _crv, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + uint256 _wbtc = IERC20(wbtc).balanceOf(address(this)); + if (_wbtc > 0) { + IERC20(wbtc).safeApprove(curve, 0); + IERC20(wbtc).safeApprove(curve, _wbtc); + ICurveFi(curve).add_liquidity([0, _wbtc, 0], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + return IVoterProxy(proxy).withdraw(gauge, want, _amount); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IVoterProxy(proxy).balanceOf(gauge); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyCurveBUSDVoterProxy.sol b/protocol/contracts/strategies/StrategyCurveBUSDVoterProxy.sol new file mode 100644 index 0000000..0bf73af --- /dev/null +++ b/protocol/contracts/strategies/StrategyCurveBUSDVoterProxy.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/curve/Gauge.sol"; +import "../../interfaces/curve/Mintr.sol"; +import "../../interfaces/uniswap/Uni.sol"; +import "../../interfaces/curve/Curve.sol"; +import "../../interfaces/yearn/IToken.sol"; +import "../../interfaces/yearn/IVoterProxy.sol"; + +contract StrategyCurveBUSDVoterProxy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0x3B3Ac5386837Dc563660FB6a0937DFAa5924333B + ); + address public constant crv = address( + 0xD533a949740bb3306d119CC777fa900bA034cd52 + ); + address public constant uni = address( + 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D + ); + address public constant weth = address( + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + ); // used for crv <> weth <> dai route + + address public constant dai = address( + 0x6B175474E89094C44Da98b954EedeAC495271d0F + ); + address public constant ydai = address( + 0xC2cB1040220768554cf699b0d863A3cd4324ce32 + ); + address public constant curve = address( + 0x79a8C46DeA5aDa233ABaFFD40F3A0A2B1e5A4F27 + ); + + address public constant gauge = address( + 0x69Fb7c45726cfE2baDeE8317005d3F94bE838840 + ); + address public constant voter = address( + 0xF147b8125d2ef93FB6965Db97D6746952a133934 + ); + + uint256 public keepCRV = 1000; + uint256 public constant keepCRVMax = 10000; + + uint256 public performanceFee = 500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCurveBUSDVoterProxy"; + } + + function setStrategist(address _strategist) external { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external { + require(msg.sender == governance, "!governance"); + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external { + require(msg.sender == governance, "!governance"); + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeTransfer(proxy, _want); + IVoterProxy(proxy).deposit(gauge, want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(crv != address(_asset), "crv"); + require(ydai != address(_asset), "ydai"); + require(dai != address(_asset), "dai"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IVoterProxy(proxy).withdrawAll(gauge, want); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + IVoterProxy(proxy).harvest(gauge, false); + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(keepCRVMax); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(uni, 0); + IERC20(crv).safeApprove(uni, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = dai; + + Uni(uni).swapExactTokensForTokens( + _crv, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + uint256 _dai = IERC20(dai).balanceOf(address(this)); + if (_dai > 0) { + IERC20(dai).safeApprove(ydai, 0); + IERC20(dai).safeApprove(ydai, _dai); + yERC20(ydai).deposit(_dai); + } + uint256 _ydai = IERC20(ydai).balanceOf(address(this)); + if (_ydai > 0) { + IERC20(ydai).safeApprove(curve, 0); + IERC20(ydai).safeApprove(curve, _ydai); + ICurveFi(curve).add_liquidity([_ydai, 0, 0, 0], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + IVoterProxy(proxy).lock(); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + return IVoterProxy(proxy).withdraw(gauge, want, _amount); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IVoterProxy(proxy).balanceOf(gauge); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyCurveCompoundVoterProxy.sol b/protocol/contracts/strategies/StrategyCurveCompoundVoterProxy.sol new file mode 100644 index 0000000..995c9ca --- /dev/null +++ b/protocol/contracts/strategies/StrategyCurveCompoundVoterProxy.sol @@ -0,0 +1,230 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/curve/Gauge.sol"; +import "../../interfaces/curve/Mintr.sol"; +import "../../interfaces/uniswap/Uni.sol"; +import "../../interfaces/curve/Curve.sol"; +import "../../interfaces/yearn/IToken.sol"; +import "../../interfaces/yearn/IVoterProxy.sol"; + +contract StrategyCurveCompoundVoterProxy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0x845838DF265Dcd2c412A1Dc9e959c7d08537f8a2 + ); + address public constant crv = address( + 0xD533a949740bb3306d119CC777fa900bA034cd52 + ); + address public constant uni = address( + 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D + ); + address public constant weth = address( + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + ); // used for crv <> weth <> dai route + + address public constant dai = address( + 0x6B175474E89094C44Da98b954EedeAC495271d0F + ); + address public constant curve = address( + 0xA2B47E3D5c44877cca798226B7B8118F9BFb7A56 + ); + + address public constant gauge = address( + 0x7ca5b0a2910B33e9759DC7dDB0413949071D7575 + ); + address public constant voter = address( + 0xF147b8125d2ef93FB6965Db97D6746952a133934 + ); + + uint256 public keepCRV = 1500; + uint256 public performanceFee = 450; + uint256 public strategistReward = 50; + uint256 public withdrawalFee = 0; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCurveCompoundVoterProxy"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external { + require(msg.sender == governance, "!governance"); + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + function setStrategistReward(uint256 _strategistReward) external { + require(msg.sender == governance, "!governance"); + strategistReward = _strategistReward; + } + + function setProxy(address _proxy) external { + require(msg.sender == governance, "!governance"); + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeTransfer(proxy, _want); + IVoterProxy(proxy).deposit(gauge, want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(crv != address(_asset), "crv"); + require(dai != address(_asset), "dai"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + return IVoterProxy(proxy).withdraw(gauge, want, _amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IVoterProxy(proxy).withdrawAll(gauge, want); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + IVoterProxy(proxy).harvest(gauge, false); + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(uni, 0); + IERC20(crv).safeApprove(uni, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = dai; + + Uni(uni).swapExactTokensForTokens( + _crv, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + uint256 _dai = IERC20(dai).balanceOf(address(this)); + if (_dai > 0) { + IERC20(dai).safeApprove(curve, 0); + IERC20(dai).safeApprove(curve, _dai); + ICurveFi(curve).add_liquidity([_dai, 0, 0], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + uint256 _reward = _want.mul(strategistReward).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + IERC20(want).safeTransfer(strategist, _reward); + deposit(); + } + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IVoterProxy(proxy).balanceOf(gauge); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyCurveEursCrvVoterProxy.sol b/protocol/contracts/strategies/StrategyCurveEursCrvVoterProxy.sol new file mode 100644 index 0000000..eb93096 --- /dev/null +++ b/protocol/contracts/strategies/StrategyCurveEursCrvVoterProxy.sol @@ -0,0 +1,265 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/curve/Gauge.sol"; +import "../../interfaces/curve/Mintr.sol"; +import "../../interfaces/uniswap/Uni.sol"; +import "../../interfaces/curve/Curve.sol"; +import "../../interfaces/yearn/IToken.sol"; +import "../../interfaces/yearn/IVoterProxy.sol"; + +contract StrategyCurveEursCrvVoterProxy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0x194eBd173F6cDacE046C53eACcE9B953F28411d1 + ); + address public constant crv = address( + 0xD533a949740bb3306d119CC777fa900bA034cd52 + ); + address public constant uni = address( + 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D + ); + address public constant weth = address( + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + ); // used for crv <> weth <> usdc <> eurs route + + address public constant eurs = address( + 0xdB25f211AB05b1c97D595516F45794528a807ad8 + ); + address public constant usdc = address( + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + ); + address public constant snx = address( + 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F + ); + address public constant curve = address( + 0x0Ce6a5fF5217e38315f87032CF90686C96627CAA + ); + + address public constant gauge = address( + 0x90Bb609649E0451E5aD952683D64BD2d1f245840 + ); + //address public constant voter = address(0xF147b8125d2ef93FB6965Db97D6746952a133934); + address public constant voter = address( + 0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6 + ); + + uint256 public keepCRV = 2000; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCurveEursCrvVoterProxy"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external { + require(msg.sender == governance, "!governance"); + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external { + require(msg.sender == governance, "!governance"); + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeTransfer(proxy, _want); + IVoterProxy(proxy).deposit(gauge, want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(crv != address(_asset), "crv"); + require(eurs != address(_asset), "eurs"); + require(snx != address(_asset), "snx"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + return IVoterProxy(proxy).withdraw(gauge, want, _amount); + } + + function withdrawToVault(uint256 amount) external { + require(msg.sender == governance, "!governance"); + amount = _withdrawSome(amount); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IVoterProxy(proxy).withdrawAll(gauge, want); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + IVoterProxy(proxy).harvest(gauge, true); + + //Swap some fraction of CRV for EURS + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(uni, 0); + IERC20(crv).safeApprove(uni, _crv); + + address[] memory path = new address[](4); + path[0] = crv; + path[1] = weth; + path[2] = usdc; + path[3] = eurs; + + Uni(uni).swapExactTokensForTokens( + _crv, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + + // Swap SNX for EURS + uint256 _snx = IERC20(snx).balanceOf(address(this)); + if (_snx > 0) { + IERC20(snx).safeApprove(uni, 0); + IERC20(snx).safeApprove(uni, _snx); + + address[] memory path = new address[](4); + path[0] = snx; + path[1] = weth; + path[2] = usdc; + path[3] = eurs; + + Uni(uni).swapExactTokensForTokens( + _snx, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + + uint256 _eurs = IERC20(eurs).balanceOf(address(this)); + if (_eurs > 0) { + IERC20(eurs).safeApprove(curve, 0); + IERC20(eurs).safeApprove(curve, _eurs); + ICurveFi(curve).add_liquidity([_eurs, 0], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IVoterProxy(proxy).balanceOf(gauge); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyCurveYVoterProxy.sol b/protocol/contracts/strategies/StrategyCurveYVoterProxy.sol new file mode 100644 index 0000000..8df19c8 --- /dev/null +++ b/protocol/contracts/strategies/StrategyCurveYVoterProxy.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/curve/Gauge.sol"; +import "../../interfaces/curve/Mintr.sol"; +import "../../interfaces/uniswap/Uni.sol"; +import "../../interfaces/curve/Curve.sol"; +import "../../interfaces/yearn/IToken.sol"; +import "../../interfaces/yearn/IVoterProxy.sol"; + +contract StrategyCurveYVoterProxy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0xdF5e0e81Dff6FAF3A7e52BA697820c5e32D806A8 + ); + address public constant crv = address( + 0xD533a949740bb3306d119CC777fa900bA034cd52 + ); + address public constant uni = address( + 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D + ); + address public constant weth = address( + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + ); // used for crv <> weth <> dai route + + address public constant dai = address( + 0x6B175474E89094C44Da98b954EedeAC495271d0F + ); + address public constant ydai = address( + 0x16de59092dAE5CcF4A1E6439D611fd0653f0Bd01 + ); + address public constant curve = address( + 0x45F783CCE6B7FF23B2ab2D70e416cdb7D6055f51 + ); + + address public constant gauge = address( + 0xFA712EE4788C042e2B7BB55E6cb8ec569C4530c1 + ); + address public constant voter = address( + 0xF147b8125d2ef93FB6965Db97D6746952a133934 + ); + + uint256 public keepCRV = 1000; + uint256 public constant keepCRVMax = 10000; + + uint256 public performanceFee = 500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyCurveYVoterProxy"; + } + + function setStrategist(address _strategist) external { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external { + require(msg.sender == governance, "!governance"); + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external { + require(msg.sender == governance, "!governance"); + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeTransfer(proxy, _want); + IVoterProxy(proxy).deposit(gauge, want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(crv != address(_asset), "crv"); + require(ydai != address(_asset), "ydai"); + require(dai != address(_asset), "dai"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IVoterProxy(proxy).withdrawAll(gauge, want); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + IVoterProxy(proxy).harvest(gauge, false); + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(keepCRVMax); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(uni, 0); + IERC20(crv).safeApprove(uni, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = dai; + + Uni(uni).swapExactTokensForTokens( + _crv, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + uint256 _dai = IERC20(dai).balanceOf(address(this)); + if (_dai > 0) { + IERC20(dai).safeApprove(ydai, 0); + IERC20(dai).safeApprove(ydai, _dai); + yERC20(ydai).deposit(_dai); + } + uint256 _ydai = IERC20(ydai).balanceOf(address(this)); + if (_ydai > 0) { + IERC20(ydai).safeApprove(curve, 0); + IERC20(ydai).safeApprove(curve, _ydai); + ICurveFi(curve).add_liquidity([_ydai, 0, 0, 0], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + return IVoterProxy(proxy).withdraw(gauge, want, _amount); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IVoterProxy(proxy).balanceOf(gauge); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyDAICurve.sol b/protocol/contracts/strategies/StrategyDAICurve.sol new file mode 100644 index 0000000..4664590 --- /dev/null +++ b/protocol/contracts/strategies/StrategyDAICurve.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/curve/Curve.sol"; +import "../../interfaces/curve/Mintr.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/yearn/IToken.sol"; + +contract StrategyDAICurve { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0x6B175474E89094C44Da98b954EedeAC495271d0F + ); + address public constant y = address( + 0x16de59092dAE5CcF4A1E6439D611fd0653f0Bd01 + ); + address public constant ycrv = address( + 0xdF5e0e81Dff6FAF3A7e52BA697820c5e32D806A8 + ); + address public constant yycrv = address( + 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c + ); + address public constant curve = address( + 0x45F783CCE6B7FF23B2ab2D70e416cdb7D6055f51 + ); + + address public constant dai = address( + 0x6B175474E89094C44Da98b954EedeAC495271d0F + ); + address public constant ydai = address( + 0x16de59092dAE5CcF4A1E6439D611fd0653f0Bd01 + ); + + address public constant usdc = address( + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + ); + address public constant yusdc = address( + 0xd6aD7a6750A7593E092a9B218d66C0A814a3436e + ); + + address public constant usdt = address( + 0xdAC17F958D2ee523a2206206994597C13D831ec7 + ); + address public constant yusdt = address( + 0x83f798e925BcD4017Eb265844FDDAbb448f1707D + ); + + address public constant tusd = address( + 0x0000000000085d4780B73119b644AE5ecd22b376 + ); + address public constant ytusd = address( + 0x73a052500105205d34Daf004eAb301916DA8190f + ); + + address public governance; + address public controller; + + constructor(address _controller) public { + governance = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyDAICurve"; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeApprove(y, 0); + IERC20(want).safeApprove(y, _want); + yERC20(y).deposit(_want); + } + uint256 _y = IERC20(y).balanceOf(address(this)); + if (_y > 0) { + IERC20(y).safeApprove(curve, 0); + IERC20(y).safeApprove(curve, _y); + ICurveFi(curve).add_liquidity([_y, 0, 0, 0], 0); + } + uint256 _ycrv = IERC20(ycrv).balanceOf(address(this)); + if (_ycrv > 0) { + IERC20(ycrv).safeApprove(yycrv, 0); + IERC20(ycrv).safeApprove(yycrv, _ycrv); + yERC20(yycrv).deposit(_ycrv); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(y != address(_asset), "sd"); + require(ycrv != address(_asset), "ycrv"); + require(yycrv != address(_asset), "yycrv"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function withdrawUnderlying(uint256 _amount) internal returns (uint256) { + IERC20(ycrv).safeApprove(curve, 0); + IERC20(ycrv).safeApprove(curve, _amount); + ICurveFi(curve).remove_liquidity(_amount, [uint256(0), 0, 0, 0]); + + uint256 _yusdc = IERC20(yusdc).balanceOf(address(this)); + uint256 _yusdt = IERC20(yusdt).balanceOf(address(this)); + uint256 _ytusd = IERC20(ytusd).balanceOf(address(this)); + + if (_yusdc > 0) { + IERC20(yusdc).safeApprove(curve, 0); + IERC20(yusdc).safeApprove(curve, _yusdc); + ICurveFi(curve).exchange(1, 0, _yusdc, 0); + } + if (_yusdt > 0) { + IERC20(yusdt).safeApprove(curve, 0); + IERC20(yusdt).safeApprove(curve, _yusdt); + ICurveFi(curve).exchange(2, 0, _yusdt, 0); + } + if (_ytusd > 0) { + IERC20(ytusd).safeApprove(curve, 0); + IERC20(ytusd).safeApprove(curve, _ytusd); + ICurveFi(curve).exchange(3, 0, _ytusd, 0); + } + + uint256 _before = IERC20(want).balanceOf(address(this)); + yERC20(ydai).withdraw(IERC20(ydai).balanceOf(address(this))); + uint256 _after = IERC20(want).balanceOf(address(this)); + + return _after.sub(_before); + } + + function _withdrawAll() internal { + uint256 _yycrv = IERC20(yycrv).balanceOf(address(this)); + if (_yycrv > 0) { + yERC20(yycrv).withdraw(_yycrv); + withdrawUnderlying(IERC20(ycrv).balanceOf(address(this))); + } + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + // calculate amount of ycrv to withdraw for amount of _want_ + uint256 _ycrv = _amount.mul(1e18).div( + ICurveFi(curve).get_virtual_price() + ); + // calculate amount of yycrv to withdraw for amount of _ycrv_ + uint256 _yycrv = _ycrv.mul(1e18).div( + yERC20(yycrv).getPricePerFullShare() + ); + uint256 _before = IERC20(ycrv).balanceOf(address(this)); + yERC20(yycrv).withdraw(_yycrv); + uint256 _after = IERC20(ycrv).balanceOf(address(this)); + return withdrawUnderlying(_after.sub(_before)); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfYYCRV() public view returns (uint256) { + return IERC20(yycrv).balanceOf(address(this)); + } + + function balanceOfYYCRVinYCRV() public view returns (uint256) { + return + balanceOfYYCRV().mul(yERC20(yycrv).getPricePerFullShare()).div( + 1e18 + ); + } + + function balanceOfYYCRVinyTUSD() public view returns (uint256) { + return + balanceOfYYCRVinYCRV().mul(ICurveFi(curve).get_virtual_price()).div( + 1e18 + ); + } + + function balanceOfYCRV() public view returns (uint256) { + return IERC20(ycrv).balanceOf(address(this)); + } + + function balanceOfYCRVyTUSD() public view returns (uint256) { + return + balanceOfYCRV().mul(ICurveFi(curve).get_virtual_price()).div(1e18); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfYYCRVinyTUSD()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyDForceUSDC.sol b/protocol/contracts/strategies/StrategyDForceUSDC.sol new file mode 100644 index 0000000..fd3287a --- /dev/null +++ b/protocol/contracts/strategies/StrategyDForceUSDC.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/dforce/Rewards.sol"; +import "../../interfaces/dforce/Token.sol"; +import "../../interfaces/uniswap/Uni.sol"; + +import "../../interfaces/yearn/IController.sol"; + +contract StrategyDForceUSDC { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // USDC + address public constant dusdc = address(0x16c9cF62d8daC4a38FB50Ae5fa5d51E9170F3179); + address public constant pool = address(0xB71dEFDd6240c45746EC58314a01dd6D833fD3b5); + address public constant df = address(0x431ad2ff6a9C365805eBaD47Ee021148d6f7DBe0); + address public constant uni = address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + address public constant weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // used for df <> weth <> usdc route + + uint256 public performanceFee = 5000; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 500; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyDForceUSDC"; + } + + function setStrategist(address _strategist) external { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeApprove(dusdc, 0); + IERC20(want).safeApprove(dusdc, _want); + dERC20(dusdc).mint(address(this), _want); + } + + uint256 _dusdc = IERC20(dusdc).balanceOf(address(this)); + if (_dusdc > 0) { + IERC20(dusdc).safeApprove(pool, 0); + IERC20(dusdc).safeApprove(pool, _dusdc); + dRewards(pool).stake(_dusdc); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(dusdc != address(_asset), "dusdc"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + dRewards(pool).exit(); + uint256 _dusdc = IERC20(dusdc).balanceOf(address(this)); + if (_dusdc > 0) { + dERC20(dusdc).redeem(address(this), _dusdc); + } + } + + function harvest() public { + require(msg.sender == strategist || msg.sender == governance, "!authorized"); + dRewards(pool).getReward(); + uint256 _df = IERC20(df).balanceOf(address(this)); + if (_df > 0) { + IERC20(df).safeApprove(uni, 0); + IERC20(df).safeApprove(uni, _df); + + address[] memory path = new address[](3); + path[0] = df; + path[1] = weth; + path[2] = want; + + Uni(uni).swapExactTokensForTokens(_df, uint256(0), path, address(this), now.add(1800)); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 _dusdc = _amount.mul(1e18).div(dERC20(dusdc).getExchangeRate()); + uint256 _before = IERC20(dusdc).balanceOf(address(this)); + dRewards(pool).withdraw(_dusdc); + uint256 _after = IERC20(dusdc).balanceOf(address(this)); + uint256 _withdrew = _after.sub(_before); + _before = IERC20(want).balanceOf(address(this)); + dERC20(dusdc).redeem(address(this), _withdrew); + _after = IERC20(want).balanceOf(address(this)); + _withdrew = _after.sub(_before); + return _withdrew; + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return (dRewards(pool).balanceOf(address(this))).mul(dERC20(dusdc).getExchangeRate()).div(1e18); + } + + function getExchangeRate() public view returns (uint256) { + return dERC20(dusdc).getExchangeRate(); + } + + function balanceOfDUSDC() public view returns (uint256) { + return dERC20(dusdc).getTokenBalance(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfDUSDC()).add(balanceOfPool()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyDForceUSDT.sol b/protocol/contracts/strategies/StrategyDForceUSDT.sol new file mode 100644 index 0000000..27fe001 --- /dev/null +++ b/protocol/contracts/strategies/StrategyDForceUSDT.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/dforce/Rewards.sol"; +import "../../interfaces/dforce/Token.sol"; +import "../../interfaces/uniswap/Uni.sol"; + +import "../../interfaces/yearn/IController.sol"; + +contract StrategyDForceUSDT { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address(0xdAC17F958D2ee523a2206206994597C13D831ec7); + address public constant d = address(0x868277d475E0e475E38EC5CdA2d9C83B5E1D9fc8); + address public constant pool = address(0x324EebDAa45829c6A8eE903aFBc7B61AF48538df); + address public constant df = address(0x431ad2ff6a9C365805eBaD47Ee021148d6f7DBe0); + address public constant uni = address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + address public constant weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // used for df <> weth <> usdc route + + uint256 public performanceFee = 5000; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + address public governance; + address public controller; + address public strategist; + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyDForceUSDT"; + } + + function setStrategist(address _strategist) external { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeApprove(d, 0); + IERC20(want).safeApprove(d, _want); + dERC20(d).mint(address(this), _want); + } + + uint256 _d = IERC20(d).balanceOf(address(this)); + if (_d > 0) { + IERC20(d).safeApprove(pool, 0); + IERC20(d).safeApprove(pool, _d); + dRewards(pool).stake(_d); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(d != address(_asset), "d"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + dRewards(pool).exit(); + uint256 _d = IERC20(d).balanceOf(address(this)); + if (_d > 0) { + dERC20(d).redeem(address(this), _d); + } + } + + function harvest() public { + require(msg.sender == strategist || msg.sender == governance, "!authorized"); + dRewards(pool).getReward(); + uint256 _df = IERC20(df).balanceOf(address(this)); + if (_df > 0) { + IERC20(df).safeApprove(uni, 0); + IERC20(df).safeApprove(uni, _df); + + address[] memory path = new address[](3); + path[0] = df; + path[1] = weth; + path[2] = want; + + Uni(uni).swapExactTokensForTokens(_df, uint256(0), path, address(this), now.add(1800)); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 _d = _amount.mul(1e18).div(dERC20(d).getExchangeRate()); + uint256 _before = IERC20(d).balanceOf(address(this)); + dRewards(pool).withdraw(_d); + uint256 _after = IERC20(d).balanceOf(address(this)); + uint256 _withdrew = _after.sub(_before); + _before = IERC20(want).balanceOf(address(this)); + dERC20(d).redeem(address(this), _withdrew); + _after = IERC20(want).balanceOf(address(this)); + _withdrew = _after.sub(_before); + return _withdrew; + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return (dRewards(pool).balanceOf(address(this))).mul(dERC20(d).getExchangeRate()).div(1e18); + } + + function getExchangeRate() public view returns (uint256) { + return dERC20(d).getExchangeRate(); + } + + function balanceOfD() public view returns (uint256) { + return dERC20(d).getTokenBalance(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfD()).add(balanceOfPool()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyMKRVaultDAIDelegate.sol b/protocol/contracts/strategies/StrategyMKRVaultDAIDelegate.sol new file mode 100644 index 0000000..8cf070d --- /dev/null +++ b/protocol/contracts/strategies/StrategyMKRVaultDAIDelegate.sol @@ -0,0 +1,457 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/maker/Maker.sol"; +import "../../interfaces/uniswap/Uni.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/yearn/IStrategy.sol"; +import "../../interfaces/yearn/IVault.sol"; + +contract StrategyMKRVaultDAIDelegate { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant token = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + address public constant want = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + address public constant weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + address public constant dai = address(0x6B175474E89094C44Da98b954EedeAC495271d0F); + + address public cdp_manager = address(0x5ef30b9986345249bc32d8928B7ee64DE9435E39); + address public vat = address(0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B); + address public mcd_join_eth_a = address(0x2F0b23f53734252Bda2277357e97e1517d6B042A); + address public mcd_join_dai = address(0x9759A6Ac90977b93B58547b4A71c78317f391A28); + address public mcd_spot = address(0x65C79fcB50Ca1594B025960e539eD7A9a6D434A3); + address public jug = address(0x19c0976f590D67707E62397C87829d896Dc0f1F1); + + address public eth_price_oracle = address(0xCF63089A8aD2a9D8BD6Bb8022f3190EB7e1eD0f1); + address public constant yVaultDAI = address(0xACd43E627e64355f1861cEC6d3a6688B31a6F952); + + address public constant unirouter = address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + + uint256 public c = 20000; + uint256 public c_safe = 30000; + uint256 public constant c_base = 10000; + + uint256 public performanceFee = 500; + uint256 public constant performanceMax = 10000; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + uint256 public strategistReward = 5000; + uint256 public constant strategistRewardMax = 10000; + + bytes32 public constant ilk = "ETH-A"; + + address public governance; + address public controller; + address public strategist; + address public harvester; + + uint256 public cdpId; + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + harvester = msg.sender; + controller = _controller; + cdpId = ManagerLike(cdp_manager).open(ilk, address(this)); + _approveAll(); + } + + function getName() external pure returns (string memory) { + return "StrategyMKRVaultDAIDelegate"; + } + + function setStrategist(address _strategist) external { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function setHarvester(address _harvester) external { + require(msg.sender == harvester || msg.sender == governance, "!allowed"); + harvester = _harvester; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + function setStrategistReward(uint256 _strategistReward) external { + require(msg.sender == governance, "!governance"); + strategistReward = _strategistReward; + } + + function setBorrowCollateralizationRatio(uint256 _c) external { + require(msg.sender == governance, "!governance"); + c = _c; + } + + function setWithdrawCollateralizationRatio(uint256 _c_safe) external { + require(msg.sender == governance, "!governance"); + c_safe = _c_safe; + } + + function setOracle(address _oracle) external { + require(msg.sender == governance, "!governance"); + eth_price_oracle = _oracle; + } + + // optional + function setMCDValue( + address _manager, + address _ethAdapter, + address _daiAdapter, + address _spot, + address _jug + ) external { + require(msg.sender == governance, "!governance"); + cdp_manager = _manager; + vat = ManagerLike(_manager).vat(); + mcd_join_eth_a = _ethAdapter; + mcd_join_dai = _daiAdapter; + mcd_spot = _spot; + jug = _jug; + } + + function _approveAll() internal { + IERC20(token).approve(mcd_join_eth_a, uint256(-1)); + IERC20(dai).approve(mcd_join_dai, uint256(-1)); + IERC20(dai).approve(yVaultDAI, uint256(-1)); + IERC20(dai).approve(unirouter, uint256(-1)); + } + + function deposit() public { + uint256 _token = IERC20(token).balanceOf(address(this)); + if (_token > 0) { + uint256 p = _getPrice(); + uint256 _draw = _token.mul(p).mul(c_base).div(c).div(1e18); + // approve adapter to use token amount + require(_checkDebtCeiling(_draw), "debt ceiling is reached!"); + _lockWETHAndDrawDAI(_token, _draw); + } + // approve yVaultDAI use DAI + IVault(yVaultDAI).depositAll(); + } + + function _getPrice() internal view returns (uint256 p) { + (uint256 _read, ) = OSMedianizer(eth_price_oracle).read(); + (uint256 _foresight, ) = OSMedianizer(eth_price_oracle).foresight(); + p = _foresight < _read ? _foresight : _read; + } + + function _checkDebtCeiling(uint256 _amt) internal view returns (bool) { + (, , , uint256 _line, ) = VatLike(vat).ilks(ilk); + uint256 _debt = getTotalDebtAmount().add(_amt); + if (_line.div(1e27) < _debt) { + return false; + } + return true; + } + + function _lockWETHAndDrawDAI(uint256 wad, uint256 wadD) internal { + address urn = ManagerLike(cdp_manager).urns(cdpId); + + // GemJoinLike(mcd_join_eth_a).gem().approve(mcd_join_eth_a, wad); + GemJoinLike(mcd_join_eth_a).join(urn, wad); + ManagerLike(cdp_manager).frob(cdpId, toInt(wad), _getDrawDart(urn, wadD)); + ManagerLike(cdp_manager).move(cdpId, address(this), wadD.mul(1e27)); + if (VatLike(vat).can(address(this), address(mcd_join_dai)) == 0) { + VatLike(vat).hope(mcd_join_dai); + } + DaiJoinLike(mcd_join_dai).exit(address(this), wadD); + } + + function _getDrawDart(address urn, uint256 wad) internal returns (int256 dart) { + uint256 rate = JugLike(jug).drip(ilk); + uint256 _dai = VatLike(vat).dai(urn); + + // If there was already enough DAI in the vat balance, just exits it without adding more debt + if (_dai < wad.mul(1e27)) { + dart = toInt(wad.mul(1e27).sub(_dai).div(rate)); + dart = uint256(dart).mul(rate) < wad.mul(1e27) ? dart + 1 : dart; + } + } + + function toInt(uint256 x) internal pure returns (int256 y) { + y = int256(x); + require(y >= 0, "int-overflow"); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(dai != address(_asset), "dai"); + require(yVaultDAI != address(_asset), "ydai"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + if (getTotalDebtAmount() != 0 && getmVaultRatio(_amount) < c_safe.mul(1e2)) { + uint256 p = _getPrice(); + _wipe(_withdrawDaiLeast(_amount.mul(p).div(1e18))); + } + + _freeWETH(_amount); + + return _amount; + } + + function _freeWETH(uint256 wad) internal { + ManagerLike(cdp_manager).frob(cdpId, -toInt(wad), 0); + ManagerLike(cdp_manager).flux(cdpId, address(this), wad); + GemJoinLike(mcd_join_eth_a).exit(address(this), wad); + } + + function _wipe(uint256 wad) internal { + // wad in DAI + address urn = ManagerLike(cdp_manager).urns(cdpId); + + DaiJoinLike(mcd_join_dai).join(urn, wad); + ManagerLike(cdp_manager).frob(cdpId, 0, _getWipeDart(VatLike(vat).dai(urn), urn)); + } + + function _getWipeDart(uint256 _dai, address urn) internal view returns (int256 dart) { + (, uint256 rate, , , ) = VatLike(vat).ilks(ilk); + (, uint256 art) = VatLike(vat).urns(ilk, urn); + + dart = toInt(_dai / rate); + dart = uint256(dart) <= art ? -dart : -toInt(art); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + _swap(IERC20(dai).balanceOf(address(this))); + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IVault(yVaultDAI).withdrawAll(); // get Dai + _wipe(getTotalDebtAmount().add(1)); // in case of edge case + _freeWETH(balanceOfmVault()); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfmVault()); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfmVault() public view returns (uint256) { + uint256 ink; + address urnHandler = ManagerLike(cdp_manager).urns(cdpId); + (ink, ) = VatLike(vat).urns(ilk, urnHandler); + return ink; + } + + function harvest() public { + require(msg.sender == strategist || msg.sender == harvester || msg.sender == governance, "!authorized"); + + uint256 v = getUnderlyingDai(); + uint256 d = getTotalDebtAmount(); + require(v > d, "profit is not realized yet!"); + uint256 profit = v.sub(d); + + uint256 _before = IERC20(want).balanceOf(address(this)); + _swap(_withdrawDaiMost(profit)); + uint256 _after = IERC20(want).balanceOf(address(this)); + + uint256 _want = _after.sub(_before); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(performanceMax); + uint256 _strategistReward = _fee.mul(strategistReward).div(strategistRewardMax); + IERC20(want).safeTransfer(strategist, _strategistReward); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee.sub(_strategistReward)); + } + + deposit(); + } + + function shouldDraw() external view returns (bool) { + uint256 _safe = c.mul(1e2); + uint256 _current = getmVaultRatio(0); + if (_current > c_base.mul(c_safe).mul(1e2)) { + _current = c_base.mul(c_safe).mul(1e2); + } + return (_current > _safe); + } + + function drawAmount() public view returns (uint256) { + uint256 _safe = c.mul(1e2); + uint256 _current = getmVaultRatio(0); + if (_current > c_base.mul(c_safe).mul(1e2)) { + _current = c_base.mul(c_safe).mul(1e2); + } + if (_current > _safe) { + uint256 _eth = balanceOfmVault(); + uint256 _diff = _current.sub(_safe); + uint256 _draw = _eth.mul(_diff).div(_safe).mul(c_base).mul(1e2).div(_current); + return _draw.mul(_getPrice()).div(1e18); + } + return 0; + } + + function draw() external { + uint256 _drawD = drawAmount(); + if (_drawD > 0) { + _lockWETHAndDrawDAI(0, _drawD); + IVault(yVaultDAI).depositAll(); + } + } + + function shouldRepay() external view returns (bool) { + uint256 _safe = c.mul(1e2); + uint256 _current = getmVaultRatio(0); + _current = _current.mul(105).div(100); // 5% buffer to avoid deposit/rebalance loops + return (_current < _safe); + } + + function repayAmount() public view returns (uint256) { + uint256 _safe = c.mul(1e2); + uint256 _current = getmVaultRatio(0); + _current = _current.mul(105).div(100); // 5% buffer to avoid deposit/rebalance loops + if (_current < _safe) { + uint256 d = getTotalDebtAmount(); + uint256 diff = _safe.sub(_current); + return d.mul(diff).div(_safe); + } + return 0; + } + + function repay() external { + uint256 free = repayAmount(); + if (free > 0) { + _wipe(_withdrawDaiLeast(free)); + } + } + + function forceRebalance(uint256 _amount) external { + require(msg.sender == governance || msg.sender == strategist || msg.sender == harvester, "!authorized"); + _wipe(_withdrawDaiLeast(_amount)); + } + + function getTotalDebtAmount() public view returns (uint256) { + uint256 art; + uint256 rate; + address urnHandler = ManagerLike(cdp_manager).urns(cdpId); + (, art) = VatLike(vat).urns(ilk, urnHandler); + (, rate, , , ) = VatLike(vat).ilks(ilk); + return art.mul(rate).div(1e27); + } + + function getmVaultRatio(uint256 amount) public view returns (uint256) { + uint256 spot; // ray + uint256 liquidationRatio; // ray + uint256 denominator = getTotalDebtAmount(); + + if (denominator == 0) { + return uint256(-1); + } + + (, , spot, , ) = VatLike(vat).ilks(ilk); + (, liquidationRatio) = SpotLike(mcd_spot).ilks(ilk); + uint256 delayedCPrice = spot.mul(liquidationRatio).div(1e27); // ray + + uint256 _balance = balanceOfmVault(); + if (_balance < amount) { + _balance = 0; + } else { + _balance = _balance.sub(amount); + } + + uint256 numerator = _balance.mul(delayedCPrice).div(1e18); // ray + return numerator.div(denominator).div(1e3); + } + + function getUnderlyingDai() public view returns (uint256) { + return IERC20(yVaultDAI).balanceOf(address(this)).mul(IVault(yVaultDAI).getPricePerFullShare()).div(1e18); + } + + function _withdrawDaiMost(uint256 _amount) internal returns (uint256) { + uint256 _shares = _amount.mul(1e18).div(IVault(yVaultDAI).getPricePerFullShare()); + + if (_shares > IERC20(yVaultDAI).balanceOf(address(this))) { + _shares = IERC20(yVaultDAI).balanceOf(address(this)); + } + + uint256 _before = IERC20(dai).balanceOf(address(this)); + IVault(yVaultDAI).withdraw(_shares); + uint256 _after = IERC20(dai).balanceOf(address(this)); + return _after.sub(_before); + } + + function _withdrawDaiLeast(uint256 _amount) internal returns (uint256) { + uint256 _shares = _amount.mul(1e18).div(IVault(yVaultDAI).getPricePerFullShare()).mul(withdrawalMax).div( + withdrawalMax.sub(withdrawalFee) + ); + + if (_shares > IERC20(yVaultDAI).balanceOf(address(this))) { + _shares = IERC20(yVaultDAI).balanceOf(address(this)); + } + + uint256 _before = IERC20(dai).balanceOf(address(this)); + IVault(yVaultDAI).withdraw(_shares); + uint256 _after = IERC20(dai).balanceOf(address(this)); + return _after.sub(_before); + } + + function _swap(uint256 _amountIn) internal { + address[] memory path = new address[](2); + path[0] = address(dai); + path[1] = address(want); + + // approve unirouter to use dai + Uni(unirouter).swapExactTokensForTokens(_amountIn, 0, path, address(this), now.add(1 days)); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyMarkerProxy.sol b/protocol/contracts/strategies/StrategyMarkerProxy.sol new file mode 100644 index 0000000..e69de29 diff --git a/protocol/contracts/strategies/StrategyProxy.sol b/protocol/contracts/strategies/StrategyProxy.sol new file mode 100644 index 0000000..c6608f7 --- /dev/null +++ b/protocol/contracts/strategies/StrategyProxy.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/yearn/IProxy.sol"; +import "../../interfaces/curve/Mintr.sol"; +import "../../interfaces/curve/FeeDistribution.sol"; + +contract StrategyProxy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + //IProxy public constant proxy = IProxy(0xF147b8125d2ef93FB6965Db97D6746952a133934); + IProxy public proxy = IProxy(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + address public mintr = address(0xd061D61a4d941c39E5453435B6345Dc261C2fcE0); + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + address public constant snx = + address(0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F); + address public gauge = address(0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB); + address public y = address(0xFA712EE4788C042e2B7BB55E6cb8ec569C4530c1); + address public sdveCRV; + address public CRV3 = address(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + FeeDistribution public feeDistribution = + FeeDistribution(0xA464e6DCda8AC41e03616F95f4BC98a13b8922Dc); + + mapping(address => bool) public strategies; + address public governance; + + // to save gas + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + constructor(address _sdveCRV) public { + governance = msg.sender; + sdveCRV = _sdveCRV; + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setProxy(IProxy _proxy) external onlyGovernance { + proxy = _proxy; + } + + function setMintr(address _mintr) external onlyGovernance { + mintr = _mintr; + } + + function setGauge(address _gauge) external onlyGovernance { + gauge = _gauge; + } + + function setY(address _y) external onlyGovernance { + y = _y; + } + + function setSdveCRV(address _sdveCRV) external onlyGovernance { + sdveCRV = _sdveCRV; + } + + function setCRV3(address _CRV3) external onlyGovernance { + CRV3 = _CRV3; + } + + function setFeeDistribution(FeeDistribution _feeDistribution) + external + onlyGovernance + { + feeDistribution = _feeDistribution; + } + + function approveStrategy(address _strategy) external onlyGovernance { + strategies[_strategy] = true; + } + + function revokeStrategy(address _strategy) external onlyGovernance { + strategies[_strategy] = false; + } + + function lock() external { + uint256 amount = IERC20(crv).balanceOf(address(proxy)); + if (amount > 0) proxy.increaseAmount(amount); // + } + + function vote(address _gauge, uint256 _amount) public { + require(strategies[msg.sender], "!strategy"); + proxy.execute( + gauge, + 0, + abi.encodeWithSignature( + "vote_for_gauge_weights(address,uint256)", + _gauge, + _amount + ) + ); + } + + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) public returns (uint256) { + require(strategies[msg.sender], "!strategy"); + uint256 _before = IERC20(_token).balanceOf(address(proxy)); + uint256 _beforeSnx = IERC20(snx).balanceOf(address(proxy)); + proxy.execute( + _gauge, + 0, + abi.encodeWithSignature("withdraw(uint256)", _amount) + ); + uint256 _after = IERC20(_token).balanceOf(address(proxy)); + uint256 _afterSnx = IERC20(snx).balanceOf(address(proxy)); + uint256 _net = _after.sub(_before); + proxy.execute( + _token, + 0, + abi.encodeWithSignature( + "transfer(address,uint256)", + msg.sender, + _net + ) + ); + uint256 _snx = _afterSnx.sub(_beforeSnx); + if (_snx > 0) { + proxy.execute( + snx, + 0, + abi.encodeWithSignature( + "transfer(address,uint256)", + msg.sender, + _snx + ) + ); + } + return _net; + } + + function balanceOf(address _gauge) public view returns (uint256) { + return IERC20(_gauge).balanceOf(address(proxy)); + } + + // withdraw all + function withdrawAll(address _gauge, address _token) + external + returns (uint256) + { + require(strategies[msg.sender], "!strategy"); + return withdraw(_gauge, _token, balanceOf(_gauge)); + } + + function deposit(address _gauge, address _token) external { + require(strategies[msg.sender], "!strategy"); + uint256 _balance = IERC20(_token).balanceOf(address(this)); + IERC20(_token).safeTransfer(address(proxy), _balance); + _balance = IERC20(_token).balanceOf(address(proxy)); + + proxy.execute( + _token, + 0, + abi.encodeWithSignature("approve(address,uint256)", _gauge, 0) + ); + proxy.execute( + _token, + 0, + abi.encodeWithSignature( + "approve(address,uint256)", + _gauge, + _balance + ) + ); + uint256 _before = IERC20(snx).balanceOf(address(proxy)); + + (bool success, ) = proxy.execute( + _gauge, + 0, + abi.encodeWithSignature("deposit(uint256)", _balance) + ); + if (!success) assert(false); + + uint256 _after = IERC20(snx).balanceOf(address(proxy)); + _balance = _after.sub(_before); + if (_balance > 0) { + proxy.execute( + snx, + 0, + abi.encodeWithSignature( + "transfer(address,uint256)", + msg.sender, + _balance + ) + ); + } + } + + function harvest(address _gauge, bool _snxRewards) external { + require(strategies[msg.sender], "!strategy"); + uint256 _before = IERC20(crv).balanceOf(address(proxy)); + proxy.execute( + mintr, + 0, + abi.encodeWithSignature("mint(address)", _gauge) + ); + uint256 _after = IERC20(crv).balanceOf(address(proxy)); + uint256 _balance = _after.sub(_before); + proxy.execute( + crv, + 0, + abi.encodeWithSignature( + "transfer(address,uint256)", + msg.sender, + _balance + ) + ); + if (_snxRewards) { + _before = IERC20(snx).balanceOf(address(proxy)); + proxy.execute( + _gauge, + 0, + abi.encodeWithSignature("claim_rewards()") + ); + _after = IERC20(snx).balanceOf(address(proxy)); + _balance = _after.sub(_before); + if (_balance > 0) { + proxy.execute( + snx, + 0, + abi.encodeWithSignature( + "transfer(address,uint256)", + msg.sender, + _balance + ) + ); + } + } + } + + function claim(address recipient) external { + require(msg.sender == sdveCRV, "!strategy"); + uint256 amount = feeDistribution.claim(address(proxy)); + if (amount > 0) { + proxy.execute( + CRV3, + 0, + abi.encodeWithSignature( + "transfer(address,uint256)", + recipient, + amount + ) + ); + } + } + + function ifTokenGetStuckInProxy(address asset, address recipient) + external + onlyGovernance + returns (uint256 balance) + { + require(asset != address(0), "invalid asset"); + require(recipient != address(0), "invalid recipient"); + balance = proxy.withdraw(IERC20(asset)); + if (balance > 0) { + IERC20(asset).safeTransfer(recipient, balance); + } + } + + // lets see + function ifTokenGetStuck(address asset, address recipient) + external + onlyGovernance + returns (uint256 balance) + { + require(asset != address(0), "invalid asset"); + require(recipient != address(0), "invalid recipient"); + balance = IERC20(asset).balanceOf(address(this)); + if (balance > 0) { + IERC20(asset).safeTransfer(recipient, balance); + } + } +} diff --git a/protocol/contracts/strategies/StrategyTUSDCurve.sol b/protocol/contracts/strategies/StrategyTUSDCurve.sol new file mode 100644 index 0000000..5e78cbd --- /dev/null +++ b/protocol/contracts/strategies/StrategyTUSDCurve.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/curve/Curve.sol"; +import "../../interfaces/uniswap/Uni.sol"; +import "../../interfaces/curve/Mintr.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/yearn/IToken.sol"; + +contract StrategyTUSDCurve { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0x0000000000085d4780B73119b644AE5ecd22b376 + ); + address public constant y = address( + 0x73a052500105205d34Daf004eAb301916DA8190f + ); + address public constant ycrv = address( + 0xdF5e0e81Dff6FAF3A7e52BA697820c5e32D806A8 + ); + address public constant yycrv = address( + 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c + ); + address public constant curve = address( + 0x45F783CCE6B7FF23B2ab2D70e416cdb7D6055f51 + ); + + address public constant dai = address( + 0x6B175474E89094C44Da98b954EedeAC495271d0F + ); + address public constant ydai = address( + 0x16de59092dAE5CcF4A1E6439D611fd0653f0Bd01 + ); + + address public constant usdc = address( + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + ); + address public constant yusdc = address( + 0xd6aD7a6750A7593E092a9B218d66C0A814a3436e + ); + + address public constant usdt = address( + 0xdAC17F958D2ee523a2206206994597C13D831ec7 + ); + address public constant yusdt = address( + 0x83f798e925BcD4017Eb265844FDDAbb448f1707D + ); + + address public constant tusd = address( + 0x0000000000085d4780B73119b644AE5ecd22b376 + ); + address public constant ytusd = address( + 0x73a052500105205d34Daf004eAb301916DA8190f + ); + + address public governance; + address public controller; + + constructor(address _controller) public { + governance = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyTUSDCurve"; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeApprove(y, 0); + IERC20(want).safeApprove(y, _want); + yERC20(y).deposit(_want); + } + uint256 _y = IERC20(y).balanceOf(address(this)); + if (_y > 0) { + IERC20(y).safeApprove(curve, 0); + IERC20(y).safeApprove(curve, _y); + ICurveFi(curve).add_liquidity([0, 0, 0, _y], 0); + } + uint256 _ycrv = IERC20(ycrv).balanceOf(address(this)); + if (_ycrv > 0) { + IERC20(ycrv).safeApprove(yycrv, 0); + IERC20(ycrv).safeApprove(yycrv, _ycrv); + yERC20(yycrv).deposit(_ycrv); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(y != address(_asset), "sd"); + require(ycrv != address(_asset), "ycrv"); + require(yycrv != address(_asset), "yycrv"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function withdrawTUSD(uint256 _amount) internal returns (uint256) { + IERC20(ycrv).safeApprove(curve, 0); + IERC20(ycrv).safeApprove(curve, _amount); + ICurveFi(curve).remove_liquidity(_amount, [uint256(0), 0, 0, 0]); + + uint256 _ydai = IERC20(ydai).balanceOf(address(this)); + uint256 _yusdc = IERC20(yusdc).balanceOf(address(this)); + uint256 _yusdt = IERC20(yusdt).balanceOf(address(this)); + + if (_ydai > 0) { + IERC20(ydai).safeApprove(curve, 0); + IERC20(ydai).safeApprove(curve, _ydai); + ICurveFi(curve).exchange(0, 3, _ydai, 0); + } + if (_yusdc > 0) { + IERC20(yusdc).safeApprove(curve, 0); + IERC20(yusdc).safeApprove(curve, _yusdc); + ICurveFi(curve).exchange(1, 3, _yusdc, 0); + } + if (_yusdt > 0) { + IERC20(yusdt).safeApprove(curve, 0); + IERC20(yusdt).safeApprove(curve, _yusdt); + ICurveFi(curve).exchange(2, 3, _yusdt, 0); + } + + uint256 _before = IERC20(want).balanceOf(address(this)); + yERC20(ytusd).withdraw(IERC20(ytusd).balanceOf(address(this))); + uint256 _after = IERC20(want).balanceOf(address(this)); + + return _after.sub(_before); + } + + function _withdrawAll() internal { + uint256 _yycrv = IERC20(yycrv).balanceOf(address(this)); + if (_yycrv > 0) { + yERC20(yycrv).withdraw(_yycrv); + withdrawTUSD(IERC20(ycrv).balanceOf(address(this))); + } + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + // calculate amount of ycrv to withdraw for amount of _want_ + uint256 _ycrv = _amount.mul(1e18).div( + ICurveFi(curve).get_virtual_price() + ); + // calculate amount of yycrv to withdraw for amount of _ycrv_ + uint256 _yycrv = _ycrv.mul(1e18).div( + yERC20(yycrv).getPricePerFullShare() + ); + uint256 _before = IERC20(ycrv).balanceOf(address(this)); + yERC20(yycrv).withdraw(_yycrv); + uint256 _after = IERC20(ycrv).balanceOf(address(this)); + return withdrawTUSD(_after.sub(_before)); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfYYCRV() public view returns (uint256) { + return IERC20(yycrv).balanceOf(address(this)); + } + + function balanceOfYYCRVinYCRV() public view returns (uint256) { + return + balanceOfYYCRV().mul(yERC20(yycrv).getPricePerFullShare()).div( + 1e18 + ); + } + + function balanceOfYYCRVinyTUSD() public view returns (uint256) { + return + balanceOfYYCRVinYCRV().mul(ICurveFi(curve).get_virtual_price()).div( + 1e18 + ); + } + + function balanceOfYCRV() public view returns (uint256) { + return IERC20(ycrv).balanceOf(address(this)); + } + + function balanceOfYCRVyTUSD() public view returns (uint256) { + return + balanceOfYCRV().mul(ICurveFi(curve).get_virtual_price()).div(1e18); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfYYCRVinyTUSD()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyVaultUSDC.sol b/protocol/contracts/strategies/StrategyVaultUSDC.sol new file mode 100644 index 0000000..05dd3f9 --- /dev/null +++ b/protocol/contracts/strategies/StrategyVaultUSDC.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/aave/Aave.sol"; +import "../../interfaces/aave/LendingPoolAddressesProvider.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/yearn/IVault.sol"; + +contract StrategyVaultUSDC { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + address public constant vault = address(0x597aD1e0c13Bfe8025993D9e79C69E1c0233522e); + + address public constant aave = address(0x24a42fD28C976A61Df5D00D0599C34c4f90748c8); + + address public governance; + address public controller; + + constructor(address _controller) public { + governance = msg.sender; + controller = _controller; + } + + function deposit() external { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance > 0) { + IERC20(want).safeApprove(address(vault), 0); + IERC20(want).safeApprove(address(vault), _balance); + IVault(vault).deposit(_balance); + } + } + + function getAave() public view returns (address) { + return LendingPoolAddressesProvider(aave).getLendingPool(); + } + + function getName() external pure returns (string memory) { + return "StrategyVaultUSDC"; + } + + function debt() external view returns (uint256) { + (, uint256 currentBorrowBalance, , , , , , , , ) = Aave(getAave()).getUserReserveData( + want, + IController(controller).vaults(address(this)) + ); + return currentBorrowBalance; + } + + function have() public view returns (uint256) { + uint256 _have = balanceOf(); + return _have; + } + + function skimmable() public view returns (uint256) { + (, uint256 currentBorrowBalance, , , , , , , , ) = Aave(getAave()).getUserReserveData( + want, + IController(controller).vaults(address(this)) + ); + uint256 _have = have(); + if (_have > currentBorrowBalance) { + return _have.sub(currentBorrowBalance); + } else { + return 0; + } + } + + function skim() external { + uint256 _balance = IERC20(want).balanceOf(address(this)); + uint256 _amount = skimmable(); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + IERC20(want).safeTransfer(controller, _amount); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(address(_asset) != address(want), "!want"); + require(address(_asset) != address(vault), "!vault"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + address _vault = IController(controller).vaults(address(this)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + balance = IERC20(want).balanceOf(address(this)); + address _vault = IController(controller).vaults(address(this)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IVault(vault).withdraw(IERC20(vault).balanceOf(address(this))); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 _redeem = IERC20(vault).balanceOf(address(this)).mul(_amount).div(balanceSavingsInToken()); + uint256 _before = IERC20(want).balanceOf(address(this)); + IVault(vault).withdraw(_redeem); + uint256 _after = IERC20(want).balanceOf(address(this)); + return _after.sub(_before); + } + + function balanceOf() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)).add(balanceSavingsInToken()); + } + + function balanceSavingsInToken() public view returns (uint256) { + return IERC20(vault).balanceOf(address(this)).mul(IVault(vault).getPricePerFullShare()).div(1e18); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/StrategyYFIGovernance.sol b/protocol/contracts/strategies/StrategyYFIGovernance.sol new file mode 100644 index 0000000..463fbb2 --- /dev/null +++ b/protocol/contracts/strategies/StrategyYFIGovernance.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../../interfaces/yearn/IGovernance.sol"; +import "../../interfaces/yearn/IToken.sol"; +import "../../interfaces/uniswap/Uni.sol"; +import "../../interfaces/curve/Curve.sol"; + +/* + + A strategy must implement the following calls; + + - deposit() + - withdraw(address) must exclude any tokens used in the yield - Controller role - withdraw should return to Controller + - withdraw(uint) - Controller | Vault role - withdraw should always return to vault + - withdrawAll() - Controller | Vault role - withdraw should always return to vault + - balanceOf() + + Where possible, strategies must remain as immutable as possible, instead of updating variables, we update the contract by linking it in the controller + +*/ + +contract StrategyYFIGovernance { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address(0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e); + address public constant gov = address(0xBa37B002AbaFDd8E89a1995dA52740bbC013D992); + address public constant curve = address(0x45F783CCE6B7FF23B2ab2D70e416cdb7D6055f51); + address public constant zap = address(0xbBC81d23Ea2c3ec7e56D39296F0cbB648873a5d3); + + address public constant reward = address(0xdF5e0e81Dff6FAF3A7e52BA697820c5e32D806A8); + address public constant usdt = address(0xdAC17F958D2ee523a2206206994597C13D831ec7); + + address public constant uni = address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + address public constant weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // used for crv <> weth <> dai route + + uint256 public fee = 500; + uint256 public constant max = 10000; + + address public governance; + address public controller; + address public strategist; + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function setFee(uint256 _fee) external { + require(msg.sender == governance, "!governance"); + fee = _fee; + } + + function setStrategist(address _strategist) external { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function deposit() public { + IERC20(want).safeApprove(gov, 0); + IERC20(want).safeApprove(gov, IERC20(want).balanceOf(address(this))); + IGovernance(gov).stake(IERC20(want).balanceOf(address(this))); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(fee).div(max); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IGovernance(gov).exit(); + } + + function harvest() public { + require(msg.sender == strategist || msg.sender == governance || msg.sender == tx.origin, "!authorized"); + IGovernance(gov).getReward(); + uint256 _balance = IERC20(reward).balanceOf(address(this)); + if (_balance > 0) { + IERC20(reward).safeApprove(zap, 0); + IERC20(reward).safeApprove(zap, _balance); + Zap(zap).remove_liquidity_one_coin(_balance, 2, 0); + } + _balance = IERC20(usdt).balanceOf(address(this)); + if (_balance > 0) { + IERC20(usdt).safeApprove(uni, 0); + IERC20(usdt).safeApprove(uni, _balance); + + address[] memory path = new address[](3); + path[0] = usdt; + path[1] = weth; + path[2] = want; + + Uni(uni).swapExactTokensForTokens(_balance, uint256(0), path, address(this), now.add(1800)); + } + if (IERC20(want).balanceOf(address(this)) > 0) { + deposit(); + } + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + IGovernance(gov).withdraw(_amount); + return _amount; + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfYGov() public view returns (uint256) { + return IGovernance(gov).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfYGov()); + } + + function voteFor(uint256 _proposal) external { + require(msg.sender == governance, "!governance"); + IGovernance(gov).voteFor(_proposal); + } + + function voteAgainst(uint256 _proposal) external { + require(msg.sender == governance, "!governance"); + IGovernance(gov).voteAgainst(_proposal); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/convex/Strategy3CrvConvex.sol b/protocol/contracts/strategies/convex/Strategy3CrvConvex.sol new file mode 100644 index 0000000..cd4a16a --- /dev/null +++ b/protocol/contracts/strategies/convex/Strategy3CrvConvex.sol @@ -0,0 +1,354 @@ +pragma solidity ^0.5.17; + +// yarn add @openzeppelin/contracts@2.5.1 +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[3] calldata, uint256) external; + + function calc_token_amount(uint256[3] calldata, bool) + external + returns (uint256); +} + +contract Strategy3CrvConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // 3crv + address public constant want = + address(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant usdc = + address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public constant voter = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public constant sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public constant curve = + address(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); + + uint256 public keepCRV = 0; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0x689440f2Ff927E1f24c72F1087E1FAF471eCe1c8); + } + + function getName() external pure returns (string memory) { + return "Strategy3CrvConvex"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(9, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(cvx != address(_asset), "cvx"); + require(crv != address(_asset), "crv"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + // slippageCRV = 100 for 1% max slippage + function harvest( + uint256 maxSlippageCRV, + uint256 maxSlippageCVX, + uint256 maxSlippageCRVAddLiquidity + ) public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = usdc; + + uint256[] memory _amounts = + Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCRV).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](3); + path[0] = cvx; + path[1] = weth; + path[2] = usdc; + + uint256[] memory _amounts = + Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCVX).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + uint256 _usdc = IERC20(usdc).balanceOf(address(this)); + + if (_usdc > 0) { + IERC20(usdc).safeApprove(curve, 0); + IERC20(usdc).safeApprove(curve, _usdc); + + uint256 _tokenAmount = + ICurveFi(curve).calc_token_amount([0, _usdc, 0], true); + + uint256 _minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + ICurveFi(curve).add_liquidity([0, _usdc, 0], _minimalAmount); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/convex/StrategyEursConvex.sol b/protocol/contracts/strategies/convex/StrategyEursConvex.sol new file mode 100644 index 0000000..0b59db7 --- /dev/null +++ b/protocol/contracts/strategies/convex/StrategyEursConvex.sol @@ -0,0 +1,421 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +// yarn add @openzeppelin/contracts@2.5.1 +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[2] calldata, uint256) external; + + function calc_token_amount(uint256[2] calldata, bool) + external + returns (uint256); +} + +interface ISwapRouter { + function uniswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external; + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + function exactInput(ExactInputParams calldata params) + external + returns (uint256 amountOut); + + function quoteExactInput(bytes calldata path, uint256 amountIn) + external + returns (uint256 amountOut); +} + +contract StrategyEursConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // eursCRV + address public constant want = + address(0x194eBd173F6cDacE046C53eACcE9B953F28411d1); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant eurs = + address(0xdB25f211AB05b1c97D595516F45794528a807ad8); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public constant usdc = + address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + address public constant voter = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public constant sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public constant curve = + address(0x0Ce6a5fF5217e38315f87032CF90686C96627CAA); + + address public constant quoter = + address(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6); + + address public constant uniswapRouterAddress = + address(0xE592427A0AEce92De3Edee1F18E0157C05861564); + + ISwapRouter public constant uniswapRouter = + ISwapRouter(uniswapRouterAddress); + + uint256 public keepCRV = 0; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0xcB8F69E0064d8cdD29cbEb45A14cf771D904BcD3); + } + + function getName() external pure returns (string memory) { + return "StrategyEursConvex"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(22, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(cvx != address(_asset), "cvx"); + require(crv != address(_asset), "crv"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + function swapToEurs(uint256 maxSlippageEURS) internal { + uint256 _weth = IERC20(weth).balanceOf(address(this)); + + IERC20(weth).safeApprove(uniswapRouterAddress, 0); + IERC20(weth).safeApprove(uniswapRouterAddress, _weth); + + uint256 amountOut = + ISwapRouter(quoter).quoteExactInput( + abi.encodePacked(weth, uint24(3000), usdc, uint24(500), eurs), + _weth + ); + + uint256 minAmountOut = + amountOut.mul(10000 - maxSlippageEURS).div(10000); + + ISwapRouter.ExactInputParams memory params = + ISwapRouter.ExactInputParams( + abi.encodePacked(weth, uint24(3000), usdc, uint24(500), eurs), + address(this), + now.add(1800), + _weth, + minAmountOut + ); + + uniswapRouter.exactInput(params); + } + + function harvest( + uint256 maxSlippageCRV, + uint256 maxSlippageCVX, + uint256 maxSlippageCRVAddLiquidity, + uint256 maxSlippageEURS + ) public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](2); + path[0] = crv; + path[1] = weth; + + uint256[] memory _amounts = + Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = + _amounts[1].mul(10000 - maxSlippageCRV).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](2); + path[0] = cvx; + path[1] = weth; + + uint256[] memory _amounts = + Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = + _amounts[1].mul(10000 - maxSlippageCVX).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + uint256 _weth = IERC20(weth).balanceOf(address(this)); + + if (_weth > 0) { + swapToEurs(maxSlippageEURS); + } + + uint256 _eurs = IERC20(eurs).balanceOf(address(this)); + + if (_eurs > 0) { + IERC20(eurs).safeApprove(curve, 0); + IERC20(eurs).safeApprove(curve, _eurs); + + uint256 _tokenAmount = + ICurveFi(curve).calc_token_amount([_eurs, 0], true); + uint256 _minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + + ICurveFi(curve).add_liquidity([_eurs, 0], _minimalAmount); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/convex/StrategyFrxConvex.sol b/protocol/contracts/strategies/convex/StrategyFrxConvex.sol new file mode 100644 index 0000000..c7d8b83 --- /dev/null +++ b/protocol/contracts/strategies/convex/StrategyFrxConvex.sol @@ -0,0 +1,429 @@ +pragma solidity ^0.5.17; + +// yarn add @openzeppelin/contracts@2.5.1 +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[3] calldata, uint256) external; + + function calc_token_amount(uint256[3] calldata, bool) + external + returns (uint256); +} + +interface IMetapool { + function add_liquidity(uint256[2] calldata, uint256) external; + + function calc_token_amount(uint256[2] calldata, bool) + external + returns (uint256); +} + +contract StrategyFrxConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // Frax3crv + address public constant want = + address(0xd632f22692FaC7611d2AA1C0D552930D43CAEd3B); + + address public constant three_crv = + address(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + + address public constant frx = + address(0x853d955aCEf822Db058eb8505911ED77F175b99e); + + address public constant fxs = + address(0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant usdc = + address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public constant voter = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public constant sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public constant uniRouter = + address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + + address public constant metapool = + address(0xd632f22692FaC7611d2AA1C0D552930D43CAEd3B); + + address public constant three_pool = + address(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); + + uint256 public keepCRV = 10000; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0xB900EF131301B307dB5eFcbed9DBb50A3e209B2e); + } + + function getName() external pure returns (string memory) { + return "StrategyFrxConvex"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(32, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(cvx != address(_asset), "cvx"); + require(crv != address(_asset), "crv"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + // slippageCRV = 100 for 1% max slippage + function harvest( + uint256 maxSlippageCVX, + uint256 maxSlippageCRV, + uint256 maxSlippageFXS, + uint256 maxSlippageCRVAddLiquidity + ) public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + uint256 _fxs = IERC20(fxs).balanceOf(address(this)); + + // sending keepCRV to voter and swap the remaining + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + if (_crv > 0) { + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = usdc; + + uint256[] memory _amounts = + Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCRV).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + } + + // swapping fxs to frx on UniV2 + if (_fxs > 0) { + IERC20(fxs).safeApprove(uniRouter, 0); + IERC20(fxs).safeApprove(uniRouter, _fxs); + + address[] memory path = new address[](2); + path[0] = fxs; + path[1] = frx; + + uint256[] memory _amounts = + Sushi(uniRouter).getAmountsOut(_fxs, path); + uint256 _minimalAmount = + _amounts[1].mul(10000 - maxSlippageFXS).div(10000); + + Sushi(uniRouter).swapExactTokensForTokens( + _fxs, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + uint256 _frx = IERC20(frx).balanceOf(address(this)); + + // swapping cvx to usdc on Sushi + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](3); + path[0] = cvx; + path[1] = weth; + path[2] = usdc; + + uint256[] memory _amounts = + Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCVX).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + uint256 _usdc = IERC20(usdc).balanceOf(address(this)); + + // add_liquidity'ing usdc to 3pool, to get 3CRV + if (_usdc > 0) { + IERC20(usdc).safeApprove(three_pool, 0); + IERC20(usdc).safeApprove(three_pool, _usdc); + + uint256 _tokenAmount = + ICurveFi(three_pool).calc_token_amount([0, _usdc, 0], true); + + uint256 __minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div( + 10000 + ); + ICurveFi(three_pool).add_liquidity( + [0, _usdc, 0], + __minimalAmount + ); + } + } + uint256 _three_crv = IERC20(three_crv).balanceOf(address(this)); + + // add_liquidity'ing frx and/or 3CRV to frax metapool for want + if (_frx > 0 || _three_crv > 0) { + IERC20(frx).safeApprove(metapool, 0); + IERC20(frx).safeApprove(metapool, _frx); + IERC20(three_crv).safeApprove(metapool, 0); + IERC20(three_crv).safeApprove(metapool, _three_crv); + + uint256 _tokenAmount = + IMetapool(metapool).calc_token_amount([_frx, _three_crv], true); + + uint256 _minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + IMetapool(metapool).add_liquidity( + [_frx, _three_crv], + _minimalAmount + ); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/convex/StrategySbtcConvex.sol b/protocol/contracts/strategies/convex/StrategySbtcConvex.sol new file mode 100644 index 0000000..caccbb4 --- /dev/null +++ b/protocol/contracts/strategies/convex/StrategySbtcConvex.sol @@ -0,0 +1,356 @@ +pragma solidity ^0.5.17; + +// yarn add @openzeppelin/contracts@2.5.1 +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +interface IBooster { + function depositAll(uint256 _pid, bool _stake) external returns (bool); +} + +interface IBaseRewardPool { + function withdrawAndUnwrap(uint256 amount, bool claim) + external + returns (bool); + + function withdrawAllAndUnwrap(bool claim) external; + + function getReward(address _account, bool _claimExtras) + external + returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} + +interface Sushi { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; + + function getAmountsOut(uint256, address[] calldata) + external + returns (uint256[] memory); +} + +interface ICurveFi { + function add_liquidity(uint256[3] calldata, uint256) external; + + function calc_token_amount(uint256[3] calldata, bool) + external + returns (uint256); +} + +contract StrategySbtcConvex { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // 3crv + address public constant want = + address(0x075b1bb99792c9E1041bA13afEf80C91a1e70fB3); + + address public constant crv = + address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant cvx = + address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + address public constant usdc = + address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + address public constant weth = + address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address public constant wbtc = + address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + + address public constant voter = + address(0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6); + + address public constant sushiRouter = + address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + address public constant curve = + address(0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714); + + uint256 public keepCRV = 0; + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + // convex booster + address public booster; + address public baseRewardPool; + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller, address _proxy) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + proxy = _proxy; + booster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + baseRewardPool = address(0xd727A5A6D1C7b31Ff9Db4Db4d24045B7dF0CFF93); + } + + function getName() external pure returns (string memory) { + return "StrategyBtcConvex"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(booster, 0); + IERC20(want).safeApprove(booster, _want); + IBooster(booster).depositAll(7, true); + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(cvx != address(_asset), "cvx"); + require(crv != address(_asset), "crv"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + uint256 wantBefore = IERC20(want).balanceOf(address(this)); + IBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false); + uint256 wantAfter = IERC20(want).balanceOf(address(this)); + return wantAfter.sub(wantBefore); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IBaseRewardPool(baseRewardPool).withdrawAllAndUnwrap(false); + } + + // slippageCRV = 100 for 1% max slippage + function harvest( + uint256 maxSlippageCRV, + uint256 maxSlippageCVX, + uint256 maxSlippageCRVAddLiquidity + ) public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + IBaseRewardPool(baseRewardPool).getReward(address(this), true); + + uint256 _crv = IERC20(crv).balanceOf(address(this)); + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(sushiRouter, 0); + IERC20(crv).safeApprove(sushiRouter, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = wbtc; + + uint256[] memory _amounts = + Sushi(sushiRouter).getAmountsOut(_crv, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCRV).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _crv, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + if (_cvx > 0) { + IERC20(cvx).safeApprove(sushiRouter, 0); + IERC20(cvx).safeApprove(sushiRouter, _cvx); + + address[] memory path = new address[](3); + path[0] = cvx; + path[1] = weth; + path[2] = wbtc; + + uint256[] memory _amounts = + Sushi(sushiRouter).getAmountsOut(_cvx, path); + uint256 _minimalAmount = + _amounts[2].mul(10000 - maxSlippageCVX).div(10000); + + Sushi(sushiRouter).swapExactTokensForTokens( + _cvx, + _minimalAmount, + path, + address(this), + now.add(1800) + ); + } + + uint256 _wbtc = IERC20(wbtc).balanceOf(address(this)); + + if (_wbtc > 0) { + IERC20(wbtc).safeApprove(curve, 0); + IERC20(wbtc).safeApprove(curve, _wbtc); + + uint256 _tokenAmount = + ICurveFi(curve).calc_token_amount([0, _wbtc, 0], true); + + uint256 _minimalAmount = + _tokenAmount.mul(10000 - maxSlippageCRVAddLiquidity).div(10000); + ICurveFi(curve).add_liquidity([0, _wbtc, 0], _minimalAmount); + } + + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + + IVoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IBaseRewardPool(baseRewardPool).balanceOf(address(this)); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/ellipsis/StrategyEllipsis3Eps.sol b/protocol/contracts/strategies/ellipsis/StrategyEllipsis3Eps.sol new file mode 100644 index 0000000..fbb815b --- /dev/null +++ b/protocol/contracts/strategies/ellipsis/StrategyEllipsis3Eps.sol @@ -0,0 +1,270 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../../interfaces/yearn/IController.sol"; +import "../../../interfaces/uniswap/Uni.sol"; +import "../../../interfaces/curve/Curve.sol"; + +interface IMultiFeeDistribution { + // Withdraw full unlocked balance and claim pending rewards + function exit() external; +} + +interface ILpTokenStaker { + // Info of each user. + struct UserInfo { + uint256 amount; + uint256 rewardDebt; + } + + // Deposit LP tokens into the contract. Also triggers a claim. + function deposit(uint256 _pid, uint256 _amount) external; + + // Withdraw LP tokens. Also triggers a claim. + function withdraw(uint256 _pid, uint256 _amount) external; + + // Claim pending rewards for one or more pools. + // Rewards are not received directly, they are minted by the rewardMinter. + function claim(uint256[] calldata _pids) external; + + function userInfo(uint256 _pid, address _user) + external + view + returns (UserInfo memory); + + function rewardMinter() external view returns (address); +} + +contract StrategyEllipsis3Eps { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0xaF4dE8E872131AE328Ce21D909C74705d3Aaf452 + ); + address public constant eps = address( + 0xA7f552078dcC247C2684336020c03648500C6d9F + ); + address public constant pancake = address( + 0x05fF2B0DB69458A0750badebc4f9e13aDd608C7F + ); + address public constant wbnb = address( + 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c + ); // used for eps <> wbnb <> busd route + + address public constant busd = address( + 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56 + ); + address public constant pool = address( + 0x160CAed03795365F3A589f10C379FfA7d75d4E76 + ); + + address public constant lpTokenStaker = address( + 0xcce949De564fE60e7f96C85e55177F8B9E4CF61b + ); + //address public constant voter = address(0xF147b8125d2ef93FB6965Db97D6746952a133934); + /* address public constant voter = address( + 0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6 + ); */ + + // uint256 public keepCRV = 1000; + uint256 public performanceFee = 1500; + // uint256 public strategistReward = 0; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyEllipsis3Eps"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + /* function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } */ + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + /* function setStrategistReward(uint256 _strategistReward) + external + onlyGovernance + { + strategistReward = _strategistReward; + } */ + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(lpTokenStaker, _want); + ILpTokenStaker(lpTokenStaker).deposit(1, _want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(eps != address(_asset), "eps"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + ILpTokenStaker(lpTokenStaker).withdraw(1, _amount); + return _amount; + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + _withdrawSome(balanceOfPool()); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + + { + uint256[] memory _pids = new uint256[](1); + _pids[0] = 1; + + // Claim all EPS rewards + ILpTokenStaker(lpTokenStaker).claim(_pids); + } + + IMultiFeeDistribution(ILpTokenStaker(lpTokenStaker).rewardMinter()) + .exit(); + + uint256 _eps = IERC20(eps).balanceOf(address(this)); + if (_eps > 0) { + IERC20(eps).safeApprove(pancake, 0); + IERC20(eps).safeApprove(pancake, _eps); + + address[] memory path = new address[](3); + path[0] = eps; + path[1] = wbnb; + path[2] = busd; + + Uni(pancake).swapExactTokensForTokens( + _eps, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + uint256 _busd = IERC20(busd).balanceOf(address(this)); + if (_busd > 0) { + IERC20(busd).safeApprove(pool, 0); + IERC20(busd).safeApprove(pool, _busd); + ICurveFi(pool).add_liquidity([_busd, 0, 0], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return ILpTokenStaker(lpTokenStaker).userInfo(1, address(this)).amount; + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/ellipsis/StrategyEllipsisBtc.sol b/protocol/contracts/strategies/ellipsis/StrategyEllipsisBtc.sol new file mode 100644 index 0000000..ebc00eb --- /dev/null +++ b/protocol/contracts/strategies/ellipsis/StrategyEllipsisBtc.sol @@ -0,0 +1,253 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../../interfaces/yearn/IController.sol"; +import "../../../interfaces/uniswap/Uni.sol"; +import "../../../interfaces/curve/Curve.sol"; + +interface IMultiFeeDistribution { + // Withdraw full unlocked balance and claim pending rewards + function exit() external; +} + +interface ILpTokenStaker { + // Info of each user. + struct UserInfo { + uint256 amount; + uint256 rewardDebt; + } + + // Deposit LP tokens into the contract. Also triggers a claim. + function deposit(uint256 _pid, uint256 _amount) external; + + // Withdraw LP tokens. Also triggers a claim. + function withdraw(uint256 _pid, uint256 _amount) external; + + // Claim pending rewards for one or more pools. + // Rewards are not received directly, they are minted by the rewardMinter. + function claim(uint256[] calldata _pids) external; + + function userInfo(uint256 _pid, address _user) + external + view + returns (UserInfo memory); + + function rewardMinter() external view returns (address); +} + +contract StrategyEllipsisBtc { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0x2a435Ecb3fcC0E316492Dc1cdd62d0F189be5640 + ); + address public constant eps = address( + 0xA7f552078dcC247C2684336020c03648500C6d9F + ); + address public constant pancake = address( + 0x05fF2B0DB69458A0750badebc4f9e13aDd608C7F + ); + address public constant wbnb = address( + 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c + ); // used for eps <> wbnb <> btcb route + + address public constant btcb = address( + 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c + ); + address public constant pool = address( + 0x2477fB288c5b4118315714ad3c7Fd7CC69b00bf9 + ); + + address public constant lpTokenStaker = address( + 0xcce949De564fE60e7f96C85e55177F8B9E4CF61b + ); + + uint256 public performanceFee = 1500; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyEllipsisBtc"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(lpTokenStaker, _want); + ILpTokenStaker(lpTokenStaker).deposit(3, _want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(eps != address(_asset), "eps"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + ILpTokenStaker(lpTokenStaker).withdraw(3, _amount); + return _amount; + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + _withdrawSome(balanceOfPool()); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + + { + uint256[] memory _pids = new uint256[](1); + _pids[0] = 3; + + // Claim all EPS rewards + ILpTokenStaker(lpTokenStaker).claim(_pids); + } + + IMultiFeeDistribution(ILpTokenStaker(lpTokenStaker).rewardMinter()) + .exit(); + + uint256 _eps = IERC20(eps).balanceOf(address(this)); + if (_eps > 0) { + IERC20(eps).safeApprove(pancake, 0); + IERC20(eps).safeApprove(pancake, _eps); + + address[] memory path = new address[](3); + path[0] = eps; + path[1] = wbnb; + path[2] = btcb; + + Uni(pancake).swapExactTokensForTokens( + _eps, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + uint256 _btcb = IERC20(btcb).balanceOf(address(this)); + if (_btcb > 0) { + IERC20(btcb).safeApprove(pool, 0); + IERC20(btcb).safeApprove(pool, _btcb); + ICurveFi(pool).add_liquidity([_btcb, 0], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return ILpTokenStaker(lpTokenStaker).userInfo(3, address(this)).amount; + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/ellipsis/StrategyEllipsisFusdt.sol b/protocol/contracts/strategies/ellipsis/StrategyEllipsisFusdt.sol new file mode 100644 index 0000000..ba2d179 --- /dev/null +++ b/protocol/contracts/strategies/ellipsis/StrategyEllipsisFusdt.sol @@ -0,0 +1,283 @@ +pragma solidity ^0.5.17; +pragma experimental ABIEncoderV2; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../../interfaces/yearn/IController.sol"; +import "../../../interfaces/uniswap/Uni.sol"; +import "../../../interfaces/curve/Curve.sol"; + +interface IMultiFeeDistribution { + // Withdraw full unlocked balance and claim pending rewards + function exit() external; +} + +interface ILpTokenStaker { + // Info of each user. + struct UserInfo { + uint256 amount; + uint256 rewardDebt; + } + + // Deposit LP tokens into the contract. Also triggers a claim. + function deposit(uint256 _pid, uint256 _amount) external; + + // Withdraw LP tokens. Also triggers a claim. + function withdraw(uint256 _pid, uint256 _amount) external; + + // Claim pending rewards for one or more pools. + // Rewards are not received directly, they are minted by the rewardMinter. + function claim(uint256[] calldata _pids) external; + + function userInfo(uint256 _pid, address _user) + external + view + returns (UserInfo memory); + + function rewardMinter() external view returns (address); +} + +contract StrategyEllipsisFusdt { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address( + 0x373410A99B64B089DFE16F1088526D399252dacE + ); + address public constant eps = address( + 0xA7f552078dcC247C2684336020c03648500C6d9F + ); + address public constant pancake = address( + 0x05fF2B0DB69458A0750badebc4f9e13aDd608C7F + ); + address public constant wbnb = address( + 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c + ); // used for eps <> wbnb <> busd route + + address public constant busd = address( + 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56 + ); + address public constant pool = address( + 0x556ea0b4c06D043806859c9490072FaadC104b63 + ); + address public constant threeEps = address( + 0xaF4dE8E872131AE328Ce21D909C74705d3Aaf452 + ); + address public constant threePool = address( + 0x160CAed03795365F3A589f10C379FfA7d75d4E76 + ); + + address public constant lpTokenStaker = address( + 0xcce949De564fE60e7f96C85e55177F8B9E4CF61b + ); + //address public constant voter = address(0xF147b8125d2ef93FB6965Db97D6746952a133934); + /* address public constant voter = address( + 0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6 + ); */ + + // uint256 public keepCRV = 1000; + uint256 public performanceFee = 1500; + // uint256 public strategistReward = 0; + uint256 public withdrawalFee = 50; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + modifier onlyGovernance() { + require(msg.sender == governance, "!governance"); + _; + } + + modifier onlyController() { + require(msg.sender == controller, "!controller"); + _; + } + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + } + + function getName() external pure returns (string memory) { + return "StrategyEllipsisFusdt"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + /* function setKeepCRV(uint256 _keepCRV) external onlyGovernance { + keepCRV = _keepCRV; + } */ + + function setWithdrawalFee(uint256 _withdrawalFee) external onlyGovernance { + withdrawalFee = _withdrawalFee; + } + + function setPerformanceFee(uint256 _performanceFee) + external + onlyGovernance + { + performanceFee = _performanceFee; + } + + /* function setStrategistReward(uint256 _strategistReward) + external + onlyGovernance + { + strategistReward = _strategistReward; + } */ + + function setProxy(address _proxy) external onlyGovernance { + proxy = _proxy; + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(lpTokenStaker, _want); + ILpTokenStaker(lpTokenStaker).deposit(2, _want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) + external + onlyController + returns (uint256 balance) + { + require(want != address(_asset), "want"); + require(eps != address(_asset), "eps"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external onlyController { + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _withdrawSome(_amount.sub(_balance)); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + ILpTokenStaker(lpTokenStaker).withdraw(2, _amount); + return _amount; + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external onlyController returns (uint256 balance) { + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + _withdrawSome(balanceOfPool()); + } + + function harvest() public { + require( + msg.sender == strategist || msg.sender == governance, + "!authorized" + ); + + { + uint256[] memory _pids = new uint256[](1); + _pids[0] = 2; + + // Claim all EPS rewards + ILpTokenStaker(lpTokenStaker).claim(_pids); + } + + IMultiFeeDistribution(ILpTokenStaker(lpTokenStaker).rewardMinter()) + .exit(); + + uint256 _eps = IERC20(eps).balanceOf(address(this)); + if (_eps > 0) { + IERC20(eps).safeApprove(pancake, 0); + IERC20(eps).safeApprove(pancake, _eps); + + address[] memory path = new address[](3); + path[0] = eps; + path[1] = wbnb; + path[2] = busd; + + Uni(pancake).swapExactTokensForTokens( + _eps, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + uint256 _busd = IERC20(busd).balanceOf(address(this)); + if (_busd > 0) { + IERC20(busd).safeApprove(threePool, 0); + IERC20(busd).safeApprove(threePool, _busd); + ICurveFi(threePool).add_liquidity([_busd, 0, 0], 0); + + uint256 _threeEps = IERC20(threeEps).balanceOf(address(this)); + + IERC20(threeEps).safeApprove(pool, 0); + IERC20(threeEps).safeApprove(pool, _threeEps); + ICurveFi(pool).add_liquidity([0, _threeEps], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + deposit(); + } + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return ILpTokenStaker(lpTokenStaker).userInfo(2, address(this)).amount; + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setController(address _controller) external onlyGovernance { + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/whitehat/GaugeWhitehat.sol b/protocol/contracts/strategies/whitehat/GaugeWhitehat.sol new file mode 100644 index 0000000..c799d4a --- /dev/null +++ b/protocol/contracts/strategies/whitehat/GaugeWhitehat.sol @@ -0,0 +1,95 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../../interfaces/yearn/IController.sol"; +import "../../../interfaces/curve/Gauge.sol"; +import "../../../interfaces/curve/Mintr.sol"; +import "../../../interfaces/uniswap/Uni.sol"; +import "../../../interfaces/curve/Curve.sol"; +import "../../../interfaces/yearn/IToken.sol"; +import "../../../interfaces/yearn/IVoterProxy.sol"; + +interface StrategyCurveEursCrvVoterProxy { + function deposit() external; +} + +contract GaugeWhitehat { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant uni = address( + 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D + ); + address public constant weth = address( + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + ); // used for crv <> weth <> usdc <> eurs route + + address public constant eurs = address( + 0xdB25f211AB05b1c97D595516F45794528a807ad8 + ); + address public constant usdc = address( + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + ); + address public constant curve = address( + 0x0Ce6a5fF5217e38315f87032CF90686C96627CAA + ); + + address public constant want = address( + 0x194eBd173F6cDacE046C53eACcE9B953F28411d1 + ); + + address public constant snx = address( + 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F + ); + + address public constant strategy = address( + 0xc8a753B38978aDD5bD26A5D1290Abc6f9f2c4f99 + ); + + event SnxRecovered(uint256 amount); + + event wantRecovered(uint256 amount); + + function deposit(uint256 amount) external { + // Swap SNX for EURS + IERC20(snx).transferFrom(msg.sender, address(this), amount); + uint256 _snx = IERC20(snx).balanceOf(address(this)); + emit SnxRecovered(_snx); + if (_snx > 0) { + IERC20(snx).safeApprove(uni, 0); + IERC20(snx).safeApprove(uni, _snx); + + address[] memory path = new address[](4); + path[0] = snx; + path[1] = weth; + path[2] = usdc; + path[3] = eurs; + + Uni(uni).swapExactTokensForTokens( + _snx, + uint256(0), + path, + address(this), + now.add(1800) + ); + } + + uint256 _eurs = IERC20(eurs).balanceOf(address(this)); + if (_eurs > 0) { + IERC20(eurs).safeApprove(curve, 0); + IERC20(eurs).safeApprove(curve, _eurs); + ICurveFi(curve).add_liquidity([_eurs, 0], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeTransfer(strategy, _want); + if (_want > 0) { + emit wantRecovered(_want); + StrategyCurveEursCrvVoterProxy(strategy).deposit(); + } + } +} diff --git a/protocol/contracts/strategies/whitehat/StrategySNXWhitehat.sol b/protocol/contracts/strategies/whitehat/StrategySNXWhitehat.sol new file mode 100644 index 0000000..b3d6a60 --- /dev/null +++ b/protocol/contracts/strategies/whitehat/StrategySNXWhitehat.sol @@ -0,0 +1,43 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../../interfaces/yearn/IController.sol"; +import "../../../interfaces/curve/Gauge.sol"; +import "../../../interfaces/curve/Mintr.sol"; +import "../../../interfaces/uniswap/Uni.sol"; +import "../../../interfaces/curve/Curve.sol"; +import "../../../interfaces/yearn/IToken.sol"; +import "../../../interfaces/yearn/IVoterProxy.sol"; + +interface StrategyProxy { + function deposit(address _gauge, address _token) external; +} + +contract StrategySNXWhitehat { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant snx = address( + 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F + ); + + address public proxy; + address public gauge; + address public owner; + + constructor(address _proxy, address _gauge) public { + proxy = _proxy; + gauge = _gauge; + owner = msg.sender; + } + + function deposit() external { + require(msg.sender == owner); + StrategyProxy(proxy).deposit(gauge, snx); + } +} diff --git a/protocol/contracts/strategies/whitehat/WhitehatStrategyProxy.sol b/protocol/contracts/strategies/whitehat/WhitehatStrategyProxy.sol new file mode 100644 index 0000000..23032ac --- /dev/null +++ b/protocol/contracts/strategies/whitehat/WhitehatStrategyProxy.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +interface IProxy { + function withdraw(IERC20 _asset) external returns (uint256 balance); +} + +interface StrategyProxy { + function lock() external; +} + +contract WhitehatStrategyProxy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public owner; + + //IProxy public constant proxy = IProxy(0xF147b8125d2ef93FB6965Db97D6746952a133934); + IProxy public constant proxy = IProxy( + 0x96032427893A22dd2a8FDb0e5fE09abEfc9E4444 + ); + address public constant crv = address( + 0xD533a949740bb3306d119CC777fa900bA034cd52 + ); + + constructor() public { + owner = msg.sender; + } + + function rescueCrv(address newProxy, address strategyProxy) external { + require(msg.sender == owner, "!authorized"); + uint256 balance = proxy.withdraw(IERC20(crv)); + IERC20(crv).safeTransfer(newProxy, balance); + StrategyProxy(strategyProxy).lock(); + } +} diff --git a/protocol/contracts/strategies/xToken/StrategyXToken.sol b/protocol/contracts/strategies/xToken/StrategyXToken.sol new file mode 100644 index 0000000..ff54d76 --- /dev/null +++ b/protocol/contracts/strategies/xToken/StrategyXToken.sol @@ -0,0 +1,197 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; + +import "../../../interfaces/yearn/IController.sol"; + +interface IxToken { + /* + * @notice Called by users buying with KNC + * @notice Users must submit ERC20 approval before calling + * @dev Deposits to Staking contract + * @dev: Mints pro rata xKNC tokens + * @param: Number of KNC to contribue + */ + function mintWithToken(uint256 kncAmountTwei) external; + + /* + * @notice Called by users burning their xKNC + * @dev Calculates pro rata KNC and redeems from Staking contract + * @dev: Exchanges for ETH if necessary and pays out to caller + * @param tokensToRedeem + * @param redeemForKnc bool: if true, redeem for KNC; otherwise ETH + * @param kyberProxy.getExpectedRate(knc => eth) + */ + function burn( + uint256 tokensToRedeemTwei, + bool redeemForKnc, + uint256 minRate + ) external; + + /* + * @notice Returns KNC balance staked to DAO + */ + function getFundKncBalanceTwei() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256 balance); + + function totalSupply() external view returns (uint256 amount); +} + +contract StrategyXToken { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public xToken; + address public want; + + uint256 public performanceFee = 1500; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + + address public governance; + address public controller; + address public strategist; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + constructor( + address _controller, + address _xToken, + address _want + ) public { + governance = msg.sender; + strategist = msg.sender; + controller = _controller; + xToken = _xToken; + want = _want; + } + + function getName() external pure returns (string memory) { + return "StrategyXKNC"; + } + + function setStrategist(address _strategist) external { + require( + msg.sender == governance || msg.sender == strategist, + "!authorized" + ); + strategist = _strategist; + } + + /* function setKeepCRV(uint256 _keepCRV) external { + require(msg.sender == governance, "!governance"); + keepCRV = _keepCRV; + } */ + + /* function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } */ + + function setPerformanceFee(uint256 _performanceFee) external { + require(msg.sender == governance, "!governance"); + performanceFee = _performanceFee; + } + + /* function setProxy(address _proxy) external { + require(msg.sender == governance, "!governance"); + proxy = _proxy; + } */ + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).approve(proxy, _want); + IxToken(xToken).mintWithToken(_want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(performanceFee).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + _amount = _amount.mul(IxToken(xToken).totalSupply()).div( + IxToken(xToken).getFundKncBalanceTwei() + ); + IxToken(xToken).burn(_amount, true, 0); + return _amount; + } + + function withdrawToVault(uint256 amount) external { + require(msg.sender == governance, "!governance"); + amount = _withdrawSome(amount); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_vault, amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + IxToken(xToken).burn(IxToken(xToken).balanceOf(address(this)), true, 0); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return IERC20(want).balanceOf(xToken); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} diff --git a/protocol/contracts/strategies/xToken/xTokenWrapper.sol b/protocol/contracts/strategies/xToken/xTokenWrapper.sol new file mode 100644 index 0000000..088ac19 --- /dev/null +++ b/protocol/contracts/strategies/xToken/xTokenWrapper.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../../temp/openzeppelin/ERC20.sol"; +import "../../temp/openzeppelin/SafeMath.sol"; + +interface IxAAVEa { + /* + * @dev Mint xAAVE using AAVE + * @notice Must run ERC20 approval first + * @param aaveAmount: AAVE to contribute + * @param affiliate: optional recipient of 25% of fees + */ + function mintWithToken(uint256 aaveAmount, address affiliate) external; + + /* + * @dev Burn xAAVE tokens + * @notice Will fail if redemption value exceeds available liquidity + * @param redeemAmount: xAAVE to redeem + * @param redeemForEth: if true, redeem xAAVE for ETH + * @param minRate: Kyber.getExpectedRate AAVE=>ETH if redeemForEth true (no-op if false) + */ + function burn( + uint256 tokenAmount, + bool redeemForEth, + uint256 minRate + ) external; + + function getFundHoldings() external view returns (uint256); + + function totalSupply() external view returns (uint256); + + function balanceOf(address) external view returns (uint256); + + function transfer(address recipient, uint256 amount) + external + returns (bool); +} + +contract xTokenWrapper is ERC20("Stake Dao xAAVEa", "sdxAAVEa") { + using SafeMath for uint256; + + address public constant aave = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; + address public xAAVEa = 0x80DC468671316E50D4E9023D3db38D3105c1C146; + address public governance; + + constructor(address _governance) public { + governance = _governance; + } + + modifier onlyGovernance() { + require(msg.sender == governance); + _; + } + + function setxAAVEa(address _xAAVEa) external onlyGovernance { + xAAVEa = _xAAVEa; + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function rescue(uint256 amount) external onlyGovernance { + IERC20(aave).transfer(governance, amount); + } + + function mintWithToken(uint256 aaveAmount) external { + require(aaveAmount > 0, "invalid aaveAmount"); + IERC20(aave).transferFrom(msg.sender, address(this), aaveAmount); + + uint256 _balBefore = IxAAVEa(xAAVEa).balanceOf(address(this)); + IERC20(aave).approve(xAAVEa, aaveAmount); + IxAAVEa(xAAVEa).mintWithToken(aaveAmount, governance); + uint256 _balAfter = IxAAVEa(xAAVEa).balanceOf(address(this)); + + super._mint(msg.sender, _balAfter.sub(_balBefore)); + } + + function burn( + uint256 tokenAmount, + bool redeemForEth, + uint256 minRate + ) external { + require(tokenAmount > 0, "invalid tokenAmount"); + + uint256 _xAaveBalBefore; + uint256 _xAaveBalAfter; + uint256 _ethBefore; + uint256 _ethAfter; + + if (redeemForEth) { + _ethBefore = address(this).balance; + _xAaveBalBefore = IxAAVEa(xAAVEa).balanceOf(address(this)); + IxAAVEa(xAAVEa).burn(tokenAmount, redeemForEth, minRate); + _xAaveBalAfter = IxAAVEa(xAAVEa).balanceOf(address(this)); + _ethAfter = address(this).balance; + (bool success, ) = + msg.sender.call{value: _ethAfter.sub(_ethBefore)}(""); + require(success, "Eth transfer failed"); + } else { + _xAaveBalBefore = IxAAVEa(xAAVEa).balanceOf(address(this)); + uint256 _aaveBalBefore = IERC20(aave).balanceOf(address(this)); + IxAAVEa(xAAVEa).burn(tokenAmount, redeemForEth, minRate); + _xAaveBalAfter = IxAAVEa(xAAVEa).balanceOf(address(this)); + uint256 _aaveBalAfter = IERC20(aave).balanceOf(address(this)); + + IERC20(aave).transfer( + msg.sender, + _aaveBalAfter.sub(_aaveBalBefore) + ); + } + + super._burn(msg.sender, _xAaveBalBefore.sub(_xAaveBalAfter)); + } + + function redeemsdxAAVEeaForxAAVEa(uint256 sdxAaveaAmount) external { + require(sdxAaveaAmount > 0, "invalid sdxAaveaAmount"); + super._burn(msg.sender, sdxAaveaAmount); + IxAAVEa(xAAVEa).transfer(msg.sender, sdxAaveaAmount); + } + + function execute( + address to, + uint256 value, + bytes calldata data + ) external returns (bool, bytes memory) { + require(msg.sender == governance, "!governance"); + (bool success, bytes memory result) = to.call{value: value}(data); + return (success, result); + } + + function pricePerFullShare() external view returns (uint256) { + return IxAAVEa(xAAVEa).getFundHoldings().div(IxAAVEa(xAAVEa).totalSupply()); + } + + receive() external payable {} +} diff --git a/protocol/contracts/strategist/CrvStrategyKeep3r.sol b/protocol/contracts/strategist/CrvStrategyKeep3r.sol new file mode 100644 index 0000000..407c549 --- /dev/null +++ b/protocol/contracts/strategist/CrvStrategyKeep3r.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.8 <0.8.0; + +import "../temp/openzeppelin/SafeMath.sol"; + +import "../../interfaces/Keep3r/IStrategyKeep3r.sol"; +import "../../interfaces/Keep3r/ICrvStrategyKeep3r.sol"; +import "../../interfaces/crv/ICrvStrategy.sol"; +import "../../interfaces/crv/ICrvClaimable.sol"; + +import "../utils/Governable.sol"; +import "../utils/CollectableDust.sol"; + +import "./Keep3rAbstract.sol"; + +contract CrvStrategyKeep3r is + Governable, + CollectableDust, + Keep3r, + IStrategyKeep3r, + ICrvStrategyKeep3r +{ + using SafeMath for uint256; + + mapping(address => uint256) public requiredHarvest; + + constructor(address _keep3r) + public + Governable(msg.sender) + CollectableDust() + Keep3r(_keep3r) + {} + + function isCrvStrategyKeep3r() external override pure returns (bool) { + return true; + } + + // Setters + function addStrategy(address _strategy, uint256 _requiredHarvest) + external + override + onlyGovernor + { + require( + requiredHarvest[_strategy] == 0, + "crv-strategy-keep3r::add-strategy:strategy-already-added" + ); + _setRequiredHarvest(_strategy, _requiredHarvest); + emit StrategyAdded(_strategy, _requiredHarvest); + } + + function updateRequiredHarvestAmount( + address _strategy, + uint256 _requiredHarvest + ) external override onlyGovernor { + require( + requiredHarvest[_strategy] > 0, + "crv-strategy-keep3r::update-required-harvest:strategy-not-added" + ); + _setRequiredHarvest(_strategy, _requiredHarvest); + emit StrategyModified(_strategy, _requiredHarvest); + } + + function removeStrategy(address _strategy) external override onlyGovernor { + require( + requiredHarvest[_strategy] > 0, + "crv-strategy-keep3r::remove-strategy:strategy-not-added" + ); + requiredHarvest[_strategy] = 0; + emit StrategyRemoved(_strategy); + } + + function setKeep3r(address _keep3r) external override onlyGovernor { + _setKeep3r(_keep3r); + emit Keep3rSet(_keep3r); + } + + function _setRequiredHarvest(address _strategy, uint256 _requiredHarvest) + internal + { + require( + _requiredHarvest > 0, + "crv-strategy-keep3r::set-required-harvest:should-not-be-zero" + ); + requiredHarvest[_strategy] = _requiredHarvest; + } + + // Getters + function calculateHarvest(address _strategy) + public + override + returns (uint256 _amount) + { + require( + requiredHarvest[_strategy] > 0, + "crv-strategy-keep3r::calculate-harvest:strategy-not-added" + ); + address _gauge = ICrvStrategy(_strategy).gauge(); + address _voter = ICrvStrategy(_strategy).voter(); + return ICrvClaimable(_gauge).claimable_tokens(_voter); + } + + function workable(address _strategy) public override returns (bool) { + require( + requiredHarvest[_strategy] > 0, + "crv-strategy-keep3r::workable:strategy-not-added" + ); + return calculateHarvest(_strategy) >= requiredHarvest[_strategy]; + } + + // Keep3r actions + function harvest(address _strategy) external override paysKeeper { + require( + workable(_strategy), + "crv-strategy-keep3r::harvest:not-workable" + ); + _harvest(_strategy); + emit HarvestedByKeeper(_strategy); + } + + // Governor keeper bypass + function forceHarvest(address _strategy) external override onlyGovernor { + _harvest(_strategy); + emit HarvestedByGovernor(_strategy); + } + + function _harvest(address _strategy) internal { + IHarvestableStrategy(_strategy).harvest(); + } + + // Governable + function setPendingGovernor(address _pendingGovernor) + external + override + onlyGovernor + { + _setPendingGovernor(_pendingGovernor); + } + + function acceptGovernor() external override onlyPendingGovernor { + _acceptGovernor(); + } + + // Collectable Dust + function sendDust( + address _to, + address _token, + uint256 _amount + ) external override onlyGovernor { + _sendDust(_to, _token, _amount); + } +} diff --git a/protocol/contracts/strategist/Keep3rAbstract.sol b/protocol/contracts/strategist/Keep3rAbstract.sol new file mode 100644 index 0000000..c28913f --- /dev/null +++ b/protocol/contracts/strategist/Keep3rAbstract.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.8 <0.8.0; + +import "../../interfaces/IKeep3rV1.sol"; + +abstract contract Keep3r { + IKeep3rV1 public keep3r; + + constructor(address _keep3r) { + _setKeep3r(_keep3r); + } + + function _setKeep3r(address _keep3r) internal { + keep3r = IKeep3rV1(_keep3r); + } + + function _isKeeper() internal { + require( + keep3r.isKeeper(msg.sender), + "keep3r::isKeeper:keeper-is-not-registered" + ); + } + + // Only checks if caller is a valid keeper, payment should be handled manually + modifier onlyKeeper() { + _isKeeper(); + _; + } + + // Checks if caller is a valid keeper, handles default payment after execution + modifier paysKeeper() { + _isKeeper(); + _; + keep3r.worked(msg.sender); + } +} diff --git a/protocol/contracts/temp/openzeppelin/Address.sol b/protocol/contracts/temp/openzeppelin/Address.sol new file mode 100644 index 0000000..0ea6bd0 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/Address.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.2 <0.8.0; + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { + size := extcodesize(account) + } + return size > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require( + address(this).balance >= amount, + "Address: insufficient balance" + ); + + // solhint-disable-next-line avoid-low-level-calls, avoid-call-value + (bool success, ) = recipient.call{value: amount}(""); + require( + success, + "Address: unable to send value, recipient may have reverted" + ); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain`call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) + internal + returns (bytes memory) + { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value + ) internal returns (bytes memory) { + return + functionCallWithValue( + target, + data, + value, + "Address: low-level call with value failed" + ); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { + require( + address(this).balance >= value, + "Address: insufficient balance for call" + ); + require(isContract(target), "Address: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.call{value: value}( + data + ); + return _verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) + internal + view + returns (bytes memory) + { + return + functionStaticCall( + target, + data, + "Address: low-level static call failed" + ); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) internal view returns (bytes memory) { + require(isContract(target), "Address: static call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.staticcall(data); + return _verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.3._ + */ + function functionDelegateCall(address target, bytes memory data) + internal + returns (bytes memory) + { + return + functionDelegateCall( + target, + data, + "Address: low-level delegate call failed" + ); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.3._ + */ + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + require(isContract(target), "Address: delegate call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.delegatecall(data); + return _verifyCallResult(success, returndata, errorMessage); + } + + function _verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) private pure returns (bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} diff --git a/protocol/contracts/temp/openzeppelin/Context.sol b/protocol/contracts/temp/openzeppelin/Context.sol new file mode 100644 index 0000000..f0ab273 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/Context.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/* + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with GSN meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal virtual view returns (address payable) { + return msg.sender; + } + + function _msgData() internal virtual view returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } +} diff --git a/protocol/contracts/temp/openzeppelin/ERC20.sol b/protocol/contracts/temp/openzeppelin/ERC20.sol new file mode 100644 index 0000000..9721263 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/ERC20.sol @@ -0,0 +1,375 @@ +// File: contracts/GSN/Context.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./SafeMath.sol"; +import "./Context.sol"; +import "./IERC20.sol"; +import "./Address.sol"; + +// File: contracts/token/ERC20/ERC20.sol + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of returning `false` on failure. This behavior is nonetheless conventional + * and does not conflict with the expectations of ERC20 applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20 is Context, IERC20 { + using SafeMath for uint256; + using Address for address; + + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @dev Sets the values for {name} and {symbol}, initializes {decimals} with + * a default value of 18. + * + * To select a different value for {decimals}, use {_setupDecimals}. + * + * All three of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name, string memory symbol) public { + _name = name; + _symbol = symbol; + _decimals = 18; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is + * called. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view returns (uint8) { + return _decimals; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public override view returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public override view returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) + public + virtual + override + returns (bool) + { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) + public + virtual + override + view + returns (uint256) + { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) + public + virtual + override + returns (bool) + { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + _approve( + sender, + _msgSender(), + _allowances[sender][_msgSender()].sub( + amount, + "ERC20: transfer amount exceeds allowance" + ) + ); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) + public + virtual + returns (bool) + { + _approve( + _msgSender(), + spender, + _allowances[_msgSender()][spender].add(addedValue) + ); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) + public + virtual + returns (bool) + { + _approve( + _msgSender(), + spender, + _allowances[_msgSender()][spender].sub( + subtractedValue, + "ERC20: decreased allowance below zero" + ) + ); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub( + amount, + "ERC20: transfer amount exceeds balance" + ); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub( + amount, + "ERC20: burn amount exceeds balance" + ); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Sets {decimals} to a value other than the default one of 18. + * + * WARNING: This function should only be called from the constructor. Most + * applications that interact with token contracts will not expect + * {decimals} to ever change, and may work incorrectly if it does. + */ + function _setupDecimals(uint8 decimals_) internal { + _decimals = decimals_; + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be to transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} +} diff --git a/protocol/contracts/temp/openzeppelin/EnumerableSet.sol b/protocol/contracts/temp/openzeppelin/EnumerableSet.sol new file mode 100644 index 0000000..a7f68ac --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/EnumerableSet.sol @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * + * ``` + * contract Example { + * // Add the library methods + * using EnumerableSet for EnumerableSet.AddressSet; + * + * // Declare a set state variable + * EnumerableSet.AddressSet private mySet; + * } + * ``` + * + * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) + * and `uint256` (`UintSet`) are supported. + */ +library EnumerableSet { + // To implement this library for multiple types with as little code + // repetition as possible, we write it in terms of a generic Set type with + // bytes32 values. + // The Set implementation uses private functions, and user-facing + // implementations (such as AddressSet) are just wrappers around the + // underlying Set. + // This means that we can only create new EnumerableSets for types that fit + // in bytes32. + + struct Set { + // Storage of set values + bytes32[] _values; + // Position of the value in the `values` array, plus 1 because index 0 + // means a value is not in the set. + mapping(bytes32 => uint256) _indexes; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function _add(Set storage set, bytes32 value) private returns (bool) { + if (!_contains(set, value)) { + set._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + set._indexes[value] = set._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function _remove(Set storage set, bytes32 value) private returns (bool) { + // We read and store the value's index to prevent multiple reads from the same storage slot + uint256 valueIndex = set._indexes[value]; + + if (valueIndex != 0) { + // Equivalent to contains(set, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 toDeleteIndex = valueIndex - 1; + uint256 lastIndex = set._values.length - 1; + + // When the value to delete is the last one, the swap operation is unnecessary. However, since this occurs + // so rarely, we still do the swap anyway to avoid the gas cost of adding an 'if' statement. + + bytes32 lastvalue = set._values[lastIndex]; + + // Move the last value to the index where the value to delete is + set._values[toDeleteIndex] = lastvalue; + // Update the index for the moved value + set._indexes[lastvalue] = toDeleteIndex + 1; // All indexes are 1-based + + // Delete the slot where the moved value was stored + set._values.pop(); + + // Delete the index for the deleted slot + delete set._indexes[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function _contains(Set storage set, bytes32 value) + private + view + returns (bool) + { + return set._indexes[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function _length(Set storage set) private view returns (uint256) { + return set._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function _at(Set storage set, uint256 index) + private + view + returns (bytes32) + { + require( + set._values.length > index, + "EnumerableSet: index out of bounds" + ); + return set._values[index]; + } + + // Bytes32Set + + struct Bytes32Set { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(Bytes32Set storage set, bytes32 value) + internal + returns (bool) + { + return _add(set._inner, value); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(Bytes32Set storage set, bytes32 value) + internal + returns (bool) + { + return _remove(set._inner, value); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(Bytes32Set storage set, bytes32 value) + internal + view + returns (bool) + { + return _contains(set._inner, value); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(Bytes32Set storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32Set storage set, uint256 index) + internal + view + returns (bytes32) + { + return _at(set._inner, index); + } + + // AddressSet + + struct AddressSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(AddressSet storage set, address value) + internal + returns (bool) + { + return _add(set._inner, bytes32(uint256(value))); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(AddressSet storage set, address value) + internal + returns (bool) + { + return _remove(set._inner, bytes32(uint256(value))); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(AddressSet storage set, address value) + internal + view + returns (bool) + { + return _contains(set._inner, bytes32(uint256(value))); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(AddressSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(AddressSet storage set, uint256 index) + internal + view + returns (address) + { + return address(uint256(_at(set._inner, index))); + } + + // UintSet + + struct UintSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(UintSet storage set, uint256 value) internal returns (bool) { + return _add(set._inner, bytes32(value)); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(UintSet storage set, uint256 value) + internal + returns (bool) + { + return _remove(set._inner, bytes32(value)); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(UintSet storage set, uint256 value) + internal + view + returns (bool) + { + return _contains(set._inner, bytes32(value)); + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(UintSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintSet storage set, uint256 index) + internal + view + returns (uint256) + { + return uint256(_at(set._inner, index)); + } +} diff --git a/protocol/contracts/temp/openzeppelin/IERC20.sol b/protocol/contracts/temp/openzeppelin/IERC20.sol new file mode 100644 index 0000000..2d9b3db --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/IERC20.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) + external + returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) + external + view + returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} diff --git a/protocol/contracts/temp/openzeppelin/Math.sol b/protocol/contracts/temp/openzeppelin/Math.sol new file mode 100644 index 0000000..04f33ba --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/Math.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a >= b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow, so we distribute + return (a / 2) + (b / 2) + (((a % 2) + (b % 2)) / 2); + } +} diff --git a/protocol/contracts/temp/openzeppelin/Ownable.sol b/protocol/contracts/temp/openzeppelin/Ownable.sol new file mode 100644 index 0000000..da45b99 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/Ownable.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./Context.sol"; + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +contract Ownable is Context { + address private _owner; + + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() internal { + address msgSender = _msgSender(); + _owner = msgSender; + emit OwnershipTransferred(address(0), msgSender); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view returns (address) { + return _owner; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(_owner == _msgSender(), "Ownable: caller is not the owner"); + _; + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + emit OwnershipTransferred(_owner, address(0)); + _owner = address(0); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current 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; + } +} diff --git a/protocol/contracts/temp/openzeppelin/ReentrancyGuard.sol b/protocol/contracts/temp/openzeppelin/ReentrancyGuard.sol new file mode 100644 index 0000000..4cc6730 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/ReentrancyGuard.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.2 <0.8.0; + +// Part: ReentrancyGuard + +// Part: OpenZeppelin/openzeppelin-contracts@3.4.0/ReentrancyGuard + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + constructor() internal { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and make it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } +} diff --git a/protocol/contracts/temp/openzeppelin/SafeERC20.sol b/protocol/contracts/temp/openzeppelin/SafeERC20.sol new file mode 100644 index 0000000..3d49afa --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/SafeERC20.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +import "./IERC20.sol"; +import "./SafeMath.sol"; +import "./Address.sol"; + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + using SafeMath for uint256; + using Address for address; + + function safeTransfer( + IERC20 token, + address to, + uint256 value + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(token.transfer.selector, to, value) + ); + } + + function safeTransferFrom( + IERC20 token, + address from, + address to, + uint256 value + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(token.transferFrom.selector, from, to, value) + ); + } + + /** + * @dev Deprecated. This function has issues similar to the ones found in + * {IERC20-approve}, and its usage is discouraged. + * + * Whenever possible, use {safeIncreaseAllowance} and + * {safeDecreaseAllowance} instead. + */ + function safeApprove( + IERC20 token, + address spender, + uint256 value + ) internal { + // safeApprove should only be called when setting an initial allowance, + // or when resetting it to zero. To increase and decrease it, use + // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' + // solhint-disable-next-line max-line-length + require( + (value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + _callOptionalReturn( + token, + abi.encodeWithSelector(token.approve.selector, spender, value) + ); + } + + function safeIncreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + uint256 newAllowance = token.allowance(address(this), spender).add( + value + ); + _callOptionalReturn( + token, + abi.encodeWithSelector( + token.approve.selector, + spender, + newAllowance + ) + ); + } + + function safeDecreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + uint256 newAllowance = token.allowance(address(this), spender).sub( + value, + "SafeERC20: decreased allowance below zero" + ); + _callOptionalReturn( + token, + abi.encodeWithSelector( + token.approve.selector, + spender, + newAllowance + ) + ); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall( + data, + "SafeERC20: low-level call failed" + ); + if (returndata.length > 0) { + // Return data is optional + // solhint-disable-next-line max-line-length + require( + abi.decode(returndata, (bool)), + "SafeERC20: ERC20 operation did not succeed" + ); + } + } +} diff --git a/protocol/contracts/temp/openzeppelin/SafeMath.sol b/protocol/contracts/temp/openzeppelin/SafeMath.sol new file mode 100644 index 0000000..2833916 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/SafeMath.sol @@ -0,0 +1,206 @@ +//SPDX-License-Identifier: MIT +// From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/Math.sol +// Subject to the MIT license. +pragma solidity >=0.5.12; + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "add: +"); + + return c; + } + + /** + * @dev Returns the addition of two unsigned integers, reverting with custom message on overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, errorMessage); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on underflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot underflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "sub: -"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on underflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot underflow. + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "mul: *"); + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, errorMessage); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. + * Reverts on division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "div: /"); + } + + /** + * @dev Returns the integer division of two unsigned integers. + * Reverts with custom message on division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "mod: %"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/ContextUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/ContextUpgradeable.sol new file mode 100644 index 0000000..e943027 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/ContextUpgradeable.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; +import "../proxy/Initializable.sol"; + +/* + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with GSN meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract ContextUpgradeable is Initializable { + function __Context_init() internal initializer { + __Context_init_unchained(); + } + + function __Context_init_unchained() internal initializer {} + + function _msgSender() internal virtual view returns (address payable) { + return msg.sender; + } + + function _msgData() internal virtual view returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/GSNRecipientERC20FeeUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/GSNRecipientERC20FeeUpgradeable.sol new file mode 100644 index 0000000..91eb93a --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/GSNRecipientERC20FeeUpgradeable.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./GSNRecipientUpgradeable.sol"; +import "../math/SafeMathUpgradeable.sol"; +import "../access/OwnableUpgradeable.sol"; +import "../token/ERC20/SafeERC20Upgradeable.sol"; +import "../token/ERC20/ERC20Upgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev A xref:ROOT:gsn-strategies.adoc#gsn-strategies[GSN strategy] that charges transaction fees in a special purpose ERC20 + * token, which we refer to as the gas payment token. The amount charged is exactly the amount of Ether charged to the + * recipient. This means that the token is essentially pegged to the value of Ether. + * + * The distribution strategy of the gas payment token to users is not defined by this contract. It's a mintable token + * whose only minter is the recipient, so the strategy must be implemented in a derived contract, making use of the + * internal {_mint} function. + */ +contract GSNRecipientERC20FeeUpgradeable is + Initializable, + GSNRecipientUpgradeable +{ + using SafeERC20Upgradeable for __unstable__ERC20OwnedUpgradeable; + using SafeMathUpgradeable for uint256; + + enum GSNRecipientERC20FeeErrorCodes {INSUFFICIENT_BALANCE} + + __unstable__ERC20OwnedUpgradeable private _token; + + /** + * @dev The arguments to the constructor are the details that the gas payment token will have: `name` and `symbol`. `decimals` is hard-coded to 18. + */ + function __GSNRecipientERC20Fee_init( + string memory name, + string memory symbol + ) internal initializer { + __Context_init_unchained(); + __GSNRecipient_init_unchained(); + __GSNRecipientERC20Fee_init_unchained(name, symbol); + } + + function __GSNRecipientERC20Fee_init_unchained( + string memory name, + string memory symbol + ) internal initializer { + _token = new __unstable__ERC20OwnedUpgradeable(); + _token.initialize(name, symbol); + } + + /** + * @dev Returns the gas payment token. + */ + function token() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(_token); + } + + /** + * @dev Internal function that mints the gas payment token. Derived contracts should expose this function in their public API, with proper access control mechanisms. + */ + function _mint(address account, uint256 amount) internal virtual { + _token.mint(account, amount); + } + + /** + * @dev Ensures that only users with enough gas payment token balance can have transactions relayed through the GSN. + */ + function acceptRelayedCall( + address, + address from, + bytes memory, + uint256 transactionFee, + uint256 gasPrice, + uint256, + uint256, + bytes memory, + uint256 maxPossibleCharge + ) public virtual override view returns (uint256, bytes memory) { + if (_token.balanceOf(from) < maxPossibleCharge) { + return + _rejectRelayedCall( + uint256(GSNRecipientERC20FeeErrorCodes.INSUFFICIENT_BALANCE) + ); + } + + return + _approveRelayedCall( + abi.encode(from, maxPossibleCharge, transactionFee, gasPrice) + ); + } + + /** + * @dev Implements the precharge to the user. The maximum possible charge (depending on gas limit, gas price, and + * fee) will be deducted from the user balance of gas payment token. Note that this is an overestimation of the + * actual charge, necessary because we cannot predict how much gas the execution will actually need. The remainder + * is returned to the user in {_postRelayedCall}. + */ + function _preRelayedCall(bytes memory context) + internal + virtual + override + returns (bytes32) + { + (address from, uint256 maxPossibleCharge) = abi.decode( + context, + (address, uint256) + ); + + // The maximum token charge is pre-charged from the user + _token.safeTransferFrom(from, address(this), maxPossibleCharge); + } + + /** + * @dev Returns to the user the extra amount that was previously charged, once the actual execution cost is known. + */ + function _postRelayedCall( + bytes memory context, + bool, + uint256 actualCharge, + bytes32 + ) internal virtual override { + ( + address from, + uint256 maxPossibleCharge, + uint256 transactionFee, + uint256 gasPrice + ) = abi.decode(context, (address, uint256, uint256, uint256)); + + // actualCharge is an _estimated_ charge, which assumes postRelayedCall will use all available gas. + // This implementation's gas cost can be roughly estimated as 10k gas, for the two SSTORE operations in an + // ERC20 transfer. + uint256 overestimation = _computeCharge( + _POST_RELAYED_CALL_MAX_GAS.sub(10000), + gasPrice, + transactionFee + ); + actualCharge = actualCharge.sub(overestimation); + + // After the relayed call has been executed and the actual charge estimated, the excess pre-charge is returned + _token.safeTransfer(from, maxPossibleCharge.sub(actualCharge)); + } + + uint256[49] private __gap; +} + +/** + * @title __unstable__ERC20Owned + * @dev An ERC20 token owned by another contract, which has minting permissions and can use transferFrom to receive + * anyone's tokens. This contract is an internal helper for GSNRecipientERC20Fee, and should not be used + * outside of this context. + */ +// solhint-disable-next-line contract-name-camelcase +contract __unstable__ERC20OwnedUpgradeable is + Initializable, + ERC20Upgradeable, + OwnableUpgradeable +{ + function initialize(string memory name, string memory symbol) + public + virtual + initializer + { + ____unstable__ERC20Owned_init(name, symbol); + } + + uint256 private constant _UINT256_MAX = 2**256 - 1; + + function ____unstable__ERC20Owned_init( + string memory name, + string memory symbol + ) internal initializer { + __Context_init_unchained(); + __ERC20_init_unchained(name, symbol); + __Ownable_init_unchained(); + ____unstable__ERC20Owned_init_unchained(name, symbol); + } + + function ____unstable__ERC20Owned_init_unchained( + string memory name, + string memory symbol + ) internal initializer {} + + // The owner (GSNRecipientERC20Fee) can mint tokens + function mint(address account, uint256 amount) public onlyOwner { + _mint(account, amount); + } + + // The owner has 'infinite' allowance for all token holders + function allowance(address tokenOwner, address spender) + public + override + view + returns (uint256) + { + if (spender == owner()) { + return _UINT256_MAX; + } else { + return super.allowance(tokenOwner, spender); + } + } + + // Allowance for the owner cannot be changed (it is always 'infinite') + function _approve( + address tokenOwner, + address spender, + uint256 value + ) internal override { + if (spender == owner()) { + return; + } else { + super._approve(tokenOwner, spender, value); + } + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public override returns (bool) { + if (recipient == owner()) { + _transfer(sender, recipient, amount); + return true; + } else { + return super.transferFrom(sender, recipient, amount); + } + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/GSNRecipientSignatureUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/GSNRecipientSignatureUpgradeable.sol new file mode 100644 index 0000000..1d17f40 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/GSNRecipientSignatureUpgradeable.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./GSNRecipientUpgradeable.sol"; +import "../cryptography/ECDSAUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev A xref:ROOT:gsn-strategies.adoc#gsn-strategies[GSN strategy] that allows relayed transactions through when they are + * accompanied by the signature of a trusted signer. The intent is for this signature to be generated by a server that + * performs validations off-chain. Note that nothing is charged to the user in this scheme. Thus, the server should make + * sure to account for this in their economic and threat model. + */ +contract GSNRecipientSignatureUpgradeable is + Initializable, + GSNRecipientUpgradeable +{ + using ECDSAUpgradeable for bytes32; + + address private _trustedSigner; + + enum GSNRecipientSignatureErrorCodes {INVALID_SIGNER} + + /** + * @dev Sets the trusted signer that is going to be producing signatures to approve relayed calls. + */ + function __GSNRecipientSignature_init(address trustedSigner) + internal + initializer + { + __Context_init_unchained(); + __GSNRecipient_init_unchained(); + __GSNRecipientSignature_init_unchained(trustedSigner); + } + + function __GSNRecipientSignature_init_unchained(address trustedSigner) + internal + initializer + { + require( + trustedSigner != address(0), + "GSNRecipientSignature: trusted signer is the zero address" + ); + _trustedSigner = trustedSigner; + } + + /** + * @dev Ensures that only transactions with a trusted signature can be relayed through the GSN. + */ + function acceptRelayedCall( + address relay, + address from, + bytes memory encodedFunction, + uint256 transactionFee, + uint256 gasPrice, + uint256 gasLimit, + uint256 nonce, + bytes memory approvalData, + uint256 + ) public virtual override view returns (uint256, bytes memory) { + bytes memory blob = abi.encodePacked( + relay, + from, + encodedFunction, + transactionFee, + gasPrice, + gasLimit, + nonce, // Prevents replays on RelayHub + getHubAddr(), // Prevents replays in multiple RelayHubs + address(this) // Prevents replays in multiple recipients + ); + if ( + keccak256(blob).toEthSignedMessageHash().recover(approvalData) == + _trustedSigner + ) { + return _approveRelayedCall(); + } else { + return + _rejectRelayedCall( + uint256(GSNRecipientSignatureErrorCodes.INVALID_SIGNER) + ); + } + } + + function _preRelayedCall(bytes memory) + internal + virtual + override + returns (bytes32) + {} + + function _postRelayedCall( + bytes memory, + bool, + uint256, + bytes32 + ) internal virtual override {} + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/GSNRecipientUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/GSNRecipientUpgradeable.sol new file mode 100644 index 0000000..c1d0910 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/GSNRecipientUpgradeable.sol @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./IRelayRecipientUpgradeable.sol"; +import "./IRelayHubUpgradeable.sol"; +import "./ContextUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev Base GSN recipient contract: includes the {IRelayRecipient} interface + * and enables GSN support on all contracts in the inheritance tree. + * + * TIP: This contract is abstract. The functions {IRelayRecipient-acceptRelayedCall}, + * {_preRelayedCall}, and {_postRelayedCall} are not implemented and must be + * provided by derived contracts. See the + * xref:ROOT:gsn-strategies.adoc#gsn-strategies[GSN strategies] for more + * information on how to use the pre-built {GSNRecipientSignature} and + * {GSNRecipientERC20Fee}, or how to write your own. + */ +abstract contract GSNRecipientUpgradeable is + Initializable, + IRelayRecipientUpgradeable, + ContextUpgradeable +{ + function __GSNRecipient_init() internal initializer { + __Context_init_unchained(); + __GSNRecipient_init_unchained(); + } + + function __GSNRecipient_init_unchained() internal initializer { + _relayHub = 0xD216153c06E857cD7f72665E0aF1d7D82172F494; + } + + // Default RelayHub address, deployed on mainnet and all testnets at the same address + address private _relayHub; + + uint256 private constant _RELAYED_CALL_ACCEPTED = 0; + uint256 private constant _RELAYED_CALL_REJECTED = 11; + + // How much gas is forwarded to postRelayedCall + uint256 internal constant _POST_RELAYED_CALL_MAX_GAS = 100000; + + /** + * @dev Emitted when a contract changes its {IRelayHub} contract to a new one. + */ + event RelayHubChanged( + address indexed oldRelayHub, + address indexed newRelayHub + ); + + /** + * @dev Returns the address of the {IRelayHub} contract for this recipient. + */ + function getHubAddr() public override view returns (address) { + return _relayHub; + } + + /** + * @dev Switches to a new {IRelayHub} instance. This method is added for future-proofing: there's no reason to not + * use the default instance. + * + * IMPORTANT: After upgrading, the {GSNRecipient} will no longer be able to receive relayed calls from the old + * {IRelayHub} instance. Additionally, all funds should be previously withdrawn via {_withdrawDeposits}. + */ + function _upgradeRelayHub(address newRelayHub) internal virtual { + address currentRelayHub = _relayHub; + require( + newRelayHub != address(0), + "GSNRecipient: new RelayHub is the zero address" + ); + require( + newRelayHub != currentRelayHub, + "GSNRecipient: new RelayHub is the current one" + ); + + emit RelayHubChanged(currentRelayHub, newRelayHub); + + _relayHub = newRelayHub; + } + + /** + * @dev Returns the version string of the {IRelayHub} for which this recipient implementation was built. If + * {_upgradeRelayHub} is used, the new {IRelayHub} instance should be compatible with this version. + */ + // This function is view for future-proofing, it may require reading from + // storage in the future. + function relayHubVersion() public view returns (string memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return "1.0.0"; + } + + /** + * @dev Withdraws the recipient's deposits in `RelayHub`. + * + * Derived contracts should expose this in an external interface with proper access control. + */ + function _withdrawDeposits(uint256 amount, address payable payee) + internal + virtual + { + IRelayHubUpgradeable(_relayHub).withdraw(amount, payee); + } + + // Overrides for Context's functions: when called from RelayHub, sender and + // data require some pre-processing: the actual sender is stored at the end + // of the call data, which in turns means it needs to be removed from it + // when handling said data. + + /** + * @dev Replacement for msg.sender. Returns the actual sender of a transaction: msg.sender for regular transactions, + * and the end-user for GSN relayed calls (where msg.sender is actually `RelayHub`). + * + * IMPORTANT: Contracts derived from {GSNRecipient} should never use `msg.sender`, and use {_msgSender} instead. + */ + function _msgSender() + internal + virtual + override + view + returns (address payable) + { + if (msg.sender != _relayHub) { + return msg.sender; + } else { + return _getRelayedCallSender(); + } + } + + /** + * @dev Replacement for msg.data. Returns the actual calldata of a transaction: msg.data for regular transactions, + * and a reduced version for GSN relayed calls (where msg.data contains additional information). + * + * IMPORTANT: Contracts derived from {GSNRecipient} should never use `msg.data`, and use {_msgData} instead. + */ + function _msgData() internal virtual override view returns (bytes memory) { + if (msg.sender != _relayHub) { + return msg.data; + } else { + return _getRelayedCallData(); + } + } + + // Base implementations for pre and post relayedCall: only RelayHub can invoke them, and data is forwarded to the + // internal hook. + + /** + * @dev See `IRelayRecipient.preRelayedCall`. + * + * This function should not be overridden directly, use `_preRelayedCall` instead. + * + * * Requirements: + * + * - the caller must be the `RelayHub` contract. + */ + function preRelayedCall(bytes memory context) + public + virtual + override + returns (bytes32) + { + require( + msg.sender == getHubAddr(), + "GSNRecipient: caller is not RelayHub" + ); + return _preRelayedCall(context); + } + + /** + * @dev See `IRelayRecipient.preRelayedCall`. + * + * Called by `GSNRecipient.preRelayedCall`, which asserts the caller is the `RelayHub` contract. Derived contracts + * must implement this function with any relayed-call preprocessing they may wish to do. + * + */ + function _preRelayedCall(bytes memory context) + internal + virtual + returns (bytes32); + + /** + * @dev See `IRelayRecipient.postRelayedCall`. + * + * This function should not be overridden directly, use `_postRelayedCall` instead. + * + * * Requirements: + * + * - the caller must be the `RelayHub` contract. + */ + function postRelayedCall( + bytes memory context, + bool success, + uint256 actualCharge, + bytes32 preRetVal + ) public virtual override { + require( + msg.sender == getHubAddr(), + "GSNRecipient: caller is not RelayHub" + ); + _postRelayedCall(context, success, actualCharge, preRetVal); + } + + /** + * @dev See `IRelayRecipient.postRelayedCall`. + * + * Called by `GSNRecipient.postRelayedCall`, which asserts the caller is the `RelayHub` contract. Derived contracts + * must implement this function with any relayed-call postprocessing they may wish to do. + * + */ + function _postRelayedCall( + bytes memory context, + bool success, + uint256 actualCharge, + bytes32 preRetVal + ) internal virtual; + + /** + * @dev Return this in acceptRelayedCall to proceed with the execution of a relayed call. Note that this contract + * will be charged a fee by RelayHub + */ + function _approveRelayedCall() + internal + pure + returns (uint256, bytes memory) + { + return _approveRelayedCall(""); + } + + /** + * @dev See `GSNRecipient._approveRelayedCall`. + * + * This overload forwards `context` to _preRelayedCall and _postRelayedCall. + */ + function _approveRelayedCall(bytes memory context) + internal + pure + returns (uint256, bytes memory) + { + return (_RELAYED_CALL_ACCEPTED, context); + } + + /** + * @dev Return this in acceptRelayedCall to impede execution of a relayed call. No fees will be charged. + */ + function _rejectRelayedCall(uint256 errorCode) + internal + pure + returns (uint256, bytes memory) + { + return (_RELAYED_CALL_REJECTED + errorCode, ""); + } + + /* + * @dev Calculates how much RelayHub will charge a recipient for using `gas` at a `gasPrice`, given a relayer's + * `serviceFee`. + */ + function _computeCharge( + uint256 gas, + uint256 gasPrice, + uint256 serviceFee + ) internal pure returns (uint256) { + // The fee is expressed as a percentage. E.g. a value of 40 stands for a 40% fee, so the recipient will be + // charged for 1.4 times the spent amount. + return (gas * gasPrice * (100 + serviceFee)) / 100; + } + + function _getRelayedCallSender() + private + pure + returns (address payable result) + { + // We need to read 20 bytes (an address) located at array index msg.data.length - 20. In memory, the array + // is prefixed with a 32-byte length value, so we first add 32 to get the memory read index. However, doing + // so would leave the address in the upper 20 bytes of the 32-byte word, which is inconvenient and would + // require bit shifting. We therefore subtract 12 from the read index so the address lands on the lower 20 + // bytes. This can always be done due to the 32-byte prefix. + + // The final memory read index is msg.data.length - 20 + 32 - 12 = msg.data.length. Using inline assembly is the + // easiest/most-efficient way to perform this operation. + + // These fields are not accessible from assembly + bytes memory array = msg.data; + uint256 index = msg.data.length; + + // solhint-disable-next-line no-inline-assembly + assembly { + // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. + result := and( + mload(add(array, index)), + 0xffffffffffffffffffffffffffffffffffffffff + ) + } + return result; + } + + function _getRelayedCallData() private pure returns (bytes memory) { + // RelayHub appends the sender address at the end of the calldata, so in order to retrieve the actual msg.data, + // we must strip the last 20 bytes (length of an address type) from it. + + uint256 actualDataLength = msg.data.length - 20; + bytes memory actualData = new bytes(actualDataLength); + + for (uint256 i = 0; i < actualDataLength; ++i) { + actualData[i] = msg.data[i]; + } + + return actualData; + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/IRelayHubUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/IRelayHubUpgradeable.sol new file mode 100644 index 0000000..06b6504 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/IRelayHubUpgradeable.sol @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Interface for `RelayHub`, the core contract of the GSN. Users should not need to interact with this contract + * directly. + * + * See the https://github.com/OpenZeppelin/openzeppelin-gsn-helpers[OpenZeppelin GSN helpers] for more information on + * how to deploy an instance of `RelayHub` on your local test network. + */ +interface IRelayHubUpgradeable { + // Relay management + + /** + * @dev Adds stake to a relay and sets its `unstakeDelay`. If the relay does not exist, it is created, and the caller + * of this function becomes its owner. If the relay already exists, only the owner can call this function. A relay + * cannot be its own owner. + * + * All Ether in this function call will be added to the relay's stake. + * Its unstake delay will be assigned to `unstakeDelay`, but the new value must be greater or equal to the current one. + * + * Emits a {Staked} event. + */ + function stake(address relayaddr, uint256 unstakeDelay) external payable; + + /** + * @dev Emitted when a relay's stake or unstakeDelay are increased + */ + event Staked(address indexed relay, uint256 stake, uint256 unstakeDelay); + + /** + * @dev Registers the caller as a relay. + * The relay must be staked for, and not be a contract (i.e. this function must be called directly from an EOA). + * + * This function can be called multiple times, emitting new {RelayAdded} events. Note that the received + * `transactionFee` is not enforced by {relayCall}. + * + * Emits a {RelayAdded} event. + */ + function registerRelay(uint256 transactionFee, string calldata url) + external; + + /** + * @dev Emitted when a relay is registered or re-registered. Looking at these events (and filtering out + * {RelayRemoved} events) lets a client discover the list of available relays. + */ + event RelayAdded( + address indexed relay, + address indexed owner, + uint256 transactionFee, + uint256 stake, + uint256 unstakeDelay, + string url + ); + + /** + * @dev Removes (deregisters) a relay. Unregistered (but staked for) relays can also be removed. + * + * Can only be called by the owner of the relay. After the relay's `unstakeDelay` has elapsed, {unstake} will be + * callable. + * + * Emits a {RelayRemoved} event. + */ + function removeRelayByOwner(address relay) external; + + /** + * @dev Emitted when a relay is removed (deregistered). `unstakeTime` is the time when unstake will be callable. + */ + event RelayRemoved(address indexed relay, uint256 unstakeTime); + + /** Deletes the relay from the system, and gives back its stake to the owner. + * + * Can only be called by the relay owner, after `unstakeDelay` has elapsed since {removeRelayByOwner} was called. + * + * Emits an {Unstaked} event. + */ + function unstake(address relay) external; + + /** + * @dev Emitted when a relay is unstaked for, including the returned stake. + */ + event Unstaked(address indexed relay, uint256 stake); + + // States a relay can be in + enum RelayState { + Unknown, // The relay is unknown to the system: it has never been staked for + Staked, // The relay has been staked for, but it is not yet active + Registered, // The relay has registered itself, and is active (can relay calls) + Removed // The relay has been removed by its owner and can no longer relay calls. It must wait for its unstakeDelay to elapse before it can unstake + } + + /** + * @dev Returns a relay's status. Note that relays can be deleted when unstaked or penalized, causing this function + * to return an empty entry. + */ + function getRelay(address relay) + external + view + returns ( + uint256 totalStake, + uint256 unstakeDelay, + uint256 unstakeTime, + address payable owner, + RelayState state + ); + + // Balance management + + /** + * @dev Deposits Ether for a contract, so that it can receive (and pay for) relayed transactions. + * + * Unused balance can only be withdrawn by the contract itself, by calling {withdraw}. + * + * Emits a {Deposited} event. + */ + function depositFor(address target) external payable; + + /** + * @dev Emitted when {depositFor} is called, including the amount and account that was funded. + */ + event Deposited( + address indexed recipient, + address indexed from, + uint256 amount + ); + + /** + * @dev Returns an account's deposits. These can be either a contract's funds, or a relay owner's revenue. + */ + function balanceOf(address target) external view returns (uint256); + + /** + * Withdraws from an account's balance, sending it back to it. Relay owners call this to retrieve their revenue, and + * contracts can use it to reduce their funding. + * + * Emits a {Withdrawn} event. + */ + function withdraw(uint256 amount, address payable dest) external; + + /** + * @dev Emitted when an account withdraws funds from `RelayHub`. + */ + event Withdrawn( + address indexed account, + address indexed dest, + uint256 amount + ); + + // Relaying + + /** + * @dev Checks if the `RelayHub` will accept a relayed operation. + * Multiple things must be true for this to happen: + * - all arguments must be signed for by the sender (`from`) + * - the sender's nonce must be the current one + * - the recipient must accept this transaction (via {acceptRelayedCall}) + * + * Returns a `PreconditionCheck` value (`OK` when the transaction can be relayed), or a recipient-specific error + * code if it returns one in {acceptRelayedCall}. + */ + function canRelay( + address relay, + address from, + address to, + bytes calldata encodedFunction, + uint256 transactionFee, + uint256 gasPrice, + uint256 gasLimit, + uint256 nonce, + bytes calldata signature, + bytes calldata approvalData + ) external view returns (uint256 status, bytes memory recipientContext); + + // Preconditions for relaying, checked by canRelay and returned as the corresponding numeric values. + enum PreconditionCheck { + OK, // All checks passed, the call can be relayed + WrongSignature, // The transaction to relay is not signed by requested sender + WrongNonce, // The provided nonce has already been used by the sender + AcceptRelayedCallReverted, // The recipient rejected this call via acceptRelayedCall + InvalidRecipientStatusCode // The recipient returned an invalid (reserved) status code + } + + /** + * @dev Relays a transaction. + * + * For this to succeed, multiple conditions must be met: + * - {canRelay} must `return PreconditionCheck.OK` + * - the sender must be a registered relay + * - the transaction's gas price must be larger or equal to the one that was requested by the sender + * - the transaction must have enough gas to not run out of gas if all internal transactions (calls to the + * recipient) use all gas available to them + * - the recipient must have enough balance to pay the relay for the worst-case scenario (i.e. when all gas is + * spent) + * + * If all conditions are met, the call will be relayed and the recipient charged. {preRelayedCall}, the encoded + * function and {postRelayedCall} will be called in that order. + * + * Parameters: + * - `from`: the client originating the request + * - `to`: the target {IRelayRecipient} contract + * - `encodedFunction`: the function call to relay, including data + * - `transactionFee`: fee (%) the relay takes over actual gas cost + * - `gasPrice`: gas price the client is willing to pay + * - `gasLimit`: gas to forward when calling the encoded function + * - `nonce`: client's nonce + * - `signature`: client's signature over all previous params, plus the relay and RelayHub addresses + * - `approvalData`: dapp-specific data forwarded to {acceptRelayedCall}. This value is *not* verified by the + * `RelayHub`, but it still can be used for e.g. a signature. + * + * Emits a {TransactionRelayed} event. + */ + function relayCall( + address from, + address to, + bytes calldata encodedFunction, + uint256 transactionFee, + uint256 gasPrice, + uint256 gasLimit, + uint256 nonce, + bytes calldata signature, + bytes calldata approvalData + ) external; + + /** + * @dev Emitted when an attempt to relay a call failed. + * + * This can happen due to incorrect {relayCall} arguments, or the recipient not accepting the relayed call. The + * actual relayed call was not executed, and the recipient not charged. + * + * The `reason` parameter contains an error code: values 1-10 correspond to `PreconditionCheck` entries, and values + * over 10 are custom recipient error codes returned from {acceptRelayedCall}. + */ + event CanRelayFailed( + address indexed relay, + address indexed from, + address indexed to, + bytes4 selector, + uint256 reason + ); + + /** + * @dev Emitted when a transaction is relayed. + * Useful when monitoring a relay's operation and relayed calls to a contract + * + * Note that the actual encoded function might be reverted: this is indicated in the `status` parameter. + * + * `charge` is the Ether value deducted from the recipient's balance, paid to the relay's owner. + */ + event TransactionRelayed( + address indexed relay, + address indexed from, + address indexed to, + bytes4 selector, + RelayCallStatus status, + uint256 charge + ); + + // Reason error codes for the TransactionRelayed event + enum RelayCallStatus { + OK, // The transaction was successfully relayed and execution successful - never included in the event + RelayedCallFailed, // The transaction was relayed, but the relayed call failed + PreRelayedFailed, // The transaction was not relayed due to preRelatedCall reverting + PostRelayedFailed, // The transaction was relayed and reverted due to postRelatedCall reverting + RecipientBalanceChanged // The transaction was relayed and reverted due to the recipient's balance changing + } + + /** + * @dev Returns how much gas should be forwarded to a call to {relayCall}, in order to relay a transaction that will + * spend up to `relayedCallStipend` gas. + */ + function requiredGas(uint256 relayedCallStipend) + external + view + returns (uint256); + + /** + * @dev Returns the maximum recipient charge, given the amount of gas forwarded, gas price and relay fee. + */ + function maxPossibleCharge( + uint256 relayedCallStipend, + uint256 gasPrice, + uint256 transactionFee + ) external view returns (uint256); + + // Relay penalization. + // Any account can penalize relays, removing them from the system immediately, and rewarding the + // reporter with half of the relay's stake. The other half is burned so that, even if the relay penalizes itself, it + // still loses half of its stake. + + /** + * @dev Penalize a relay that signed two transactions using the same nonce (making only the first one valid) and + * different data (gas price, gas limit, etc. may be different). + * + * The (unsigned) transaction data and signature for both transactions must be provided. + */ + function penalizeRepeatedNonce( + bytes calldata unsignedTx1, + bytes calldata signature1, + bytes calldata unsignedTx2, + bytes calldata signature2 + ) external; + + /** + * @dev Penalize a relay that sent a transaction that didn't target ``RelayHub``'s {registerRelay} or {relayCall}. + */ + function penalizeIllegalTransaction( + bytes calldata unsignedTx, + bytes calldata signature + ) external; + + /** + * @dev Emitted when a relay is penalized. + */ + event Penalized(address indexed relay, address sender, uint256 amount); + + /** + * @dev Returns an account's nonce in `RelayHub`. + */ + function getNonce(address from) external view returns (uint256); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/IRelayRecipientUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/IRelayRecipientUpgradeable.sol new file mode 100644 index 0000000..58dfe26 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/IRelayRecipientUpgradeable.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Base interface for a contract that will be called via the GSN from {IRelayHub}. + * + * TIP: You don't need to write an implementation yourself! Inherit from {GSNRecipient} instead. + */ +interface IRelayRecipientUpgradeable { + /** + * @dev Returns the address of the {IRelayHub} instance this recipient interacts with. + */ + function getHubAddr() external view returns (address); + + /** + * @dev Called by {IRelayHub} to validate if this recipient accepts being charged for a relayed call. Note that the + * recipient will be charged regardless of the execution result of the relayed call (i.e. if it reverts or not). + * + * The relay request was originated by `from` and will be served by `relay`. `encodedFunction` is the relayed call + * calldata, so its first four bytes are the function selector. The relayed call will be forwarded `gasLimit` gas, + * and the transaction executed with a gas price of at least `gasPrice`. ``relay``'s fee is `transactionFee`, and the + * recipient will be charged at most `maxPossibleCharge` (in wei). `nonce` is the sender's (`from`) nonce for + * replay attack protection in {IRelayHub}, and `approvalData` is a optional parameter that can be used to hold a signature + * over all or some of the previous values. + * + * Returns a tuple, where the first value is used to indicate approval (0) or rejection (custom non-zero error code, + * values 1 to 10 are reserved) and the second one is data to be passed to the other {IRelayRecipient} functions. + * + * {acceptRelayedCall} is called with 50k gas: if it runs out during execution, the request will be considered + * rejected. A regular revert will also trigger a rejection. + */ + function acceptRelayedCall( + address relay, + address from, + bytes calldata encodedFunction, + uint256 transactionFee, + uint256 gasPrice, + uint256 gasLimit, + uint256 nonce, + bytes calldata approvalData, + uint256 maxPossibleCharge + ) external view returns (uint256, bytes memory); + + /** + * @dev Called by {IRelayHub} on approved relay call requests, before the relayed call is executed. This allows to e.g. + * pre-charge the sender of the transaction. + * + * `context` is the second value returned in the tuple by {acceptRelayedCall}. + * + * Returns a value to be passed to {postRelayedCall}. + * + * {preRelayedCall} is called with 100k gas: if it runs out during execution or otherwise reverts, the relayed call + * will not be executed, but the recipient will still be charged for the transaction's cost. + */ + function preRelayedCall(bytes calldata context) external returns (bytes32); + + /** + * @dev Called by {IRelayHub} on approved relay call requests, after the relayed call is executed. This allows to e.g. + * charge the user for the relayed call costs, return any overcharges from {preRelayedCall}, or perform + * contract-specific bookkeeping. + * + * `context` is the second value returned in the tuple by {acceptRelayedCall}. `success` is the execution status of + * the relayed call. `actualCharge` is an estimate of how much the recipient will be charged for the transaction, + * not including any gas used by {postRelayedCall} itself. `preRetVal` is {preRelayedCall}'s return value. + * + * + * {postRelayedCall} is called with 100k gas: if it runs out during execution or otherwise reverts, the relayed call + * and the call to {preRelayedCall} will be reverted retroactively, but the recipient will still be charged for the + * transaction's cost. + */ + function postRelayedCall( + bytes calldata context, + bool success, + uint256 actualCharge, + bytes32 preRetVal + ) external; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/README.adoc new file mode 100644 index 0000000..cba87cf --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/GSN/README.adoc @@ -0,0 +1,31 @@ += Gas Station Network (GSN) + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/gsn + +This set of contracts provide all the tools required to make a contract callable via the https://gsn.openzeppelin.com[Gas Station Network]. + +TIP: If you're new to the GSN, head over to our xref:learn::sending-gasless-transactions.adoc[overview of the system] and basic guide to xref:ROOT:gsn.adoc[creating a GSN-capable contract]. + +The core contract a recipient must inherit from is {GSNRecipient}: it includes all necessary interfaces, as well as some helper methods to make interacting with the GSN easier. + +Utilities to make writing xref:ROOT:gsn-strategies.adoc[GSN strategies] easy are available in {GSNRecipient}, or you can simply use one of our pre-made strategies: + +* {GSNRecipientERC20Fee} charges the end user for gas costs in an application-specific xref:ROOT:tokens.adoc#ERC20[ERC20 token] +* {GSNRecipientSignature} accepts all relayed calls that have been signed by a trusted third party (e.g. a private key in a backend) + +You can also take a look at the two contract interfaces that make up the GSN protocol: {IRelayRecipient} and {IRelayHub}, but you won't need to use those directly. + +== Recipient + +{{GSNRecipient}} + +== Strategies + +{{GSNRecipientSignature}} +{{GSNRecipientERC20Fee}} + +== Protocol + +{{IRelayRecipient}} +{{IRelayHub}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/README.md b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/README.md new file mode 100644 index 0000000..fa1dc68 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/README.md @@ -0,0 +1,86 @@ +# OpenZeppelin OpenZeppelin Contracts Upgradeable + +[![Docs](https://img.shields.io/badge/docs-%F0%9F%93%84-blue)](https://docs.openzeppelin.com/contracts/upgradeable) +[![NPM Package](https://img.shields.io/npm/v/@openzeppelin/contracts-upgradeable.svg)](https://www.npmjs.org/package/@openzeppelin/contracts-upgradeable) + +This repository hosts the Upgradeable variant of [OpenZeppelin Contracts], meant for use in upgradeable contracts. This variant is available as separate package called `@openzeppelin/contracts-upgradeable`. + +[openzeppelin contracts]: https://github.com/OpenZeppelin/openzeppelin-contracts + +It follows all of the rules for xref:upgrades-plugins::writing-upgradeable.adoc[Writing Upgradeable Contracts]: constructors are replaced by initializer functions, state variables are initialized in initializer functions, and we additionally check for storage incompatibilities across minor versions. + +[writing upgradeable contracts]: https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable + +## Overview + +### Installation + +```console +$ npm install @openzeppelin/contracts-upgradeable +``` + +### Usage + +The package replicates the structure of the main OpenZeppelin Contracts package, but every file and contract has the suffix `Upgradeable`. + +```diff +-import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; ++import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; + +-contract MyCollectible is ERC721 { ++contract MyCollectible is ERC721Upgradeable { +``` + +Constructors are replaced by internal initializer functions following the naming convention `__{ContractName}_init`. Since these are internal, you must always define your own public initializer function and call the parent initializer of the contract you extend. + +```diff +- constructor() ERC721("MyCollectible", "MCO") public { ++ function initialize() initializer public { ++ __ERC721_init("MyCollectible", "MCO"); + } +``` + +> **Caution** +> +> Use with multiple inheritance requires special care. Initializer functions are not linearized by the compiler like constructors. Because of this, each `__{ContractName}_init` function embeds the linearized calls to all parent initializers. As a consequence, calling two of these `init` functions can potentially initialize the same contract twice. +> +> The function `__{ContractName}_init_unchained` found in every contract is the initializer function minus the calls to parent initializers, and can be used to avoid the double initialization problem, but doing this manually is not recommended. We hope to be able to implement safety checks for this in future versions of the Upgrades Plugins. + +_If you're new to smart contract development, head to [Developing Smart Contracts](https://docs.openzeppelin.com/learn/developing-smart-contracts) to learn about creating a new project and compiling your contracts._ + +To keep your system secure, you should **always** use the installed code as-is, and neither copy-paste it from online sources, nor modify it yourself. The library is designed so that only the contracts and functions you use are deployed, so you don't need to worry about it needlessly increasing gas costs. + +## Learn More + +The guides in the [docs site](https://docs.openzeppelin.com/contracts) will teach about different concepts, and how to use the related contracts that OpenZeppelin Contracts provides: + +- [Access Control](https://docs.openzeppelin.com/contracts/access-control): decide who can perform each of the actions on your system. +- [Tokens](https://docs.openzeppelin.com/contracts/tokens): create tradeable assets or collectives, and distribute them via [Crowdsales](https://docs.openzeppelin.com/contracts/crowdsales). +- [Gas Station Network](https://docs.openzeppelin.com/contracts/gsn): let your users interact with your contracts without having to pay for gas themselves. +- [Utilities](https://docs.openzeppelin.com/contracts/utilities): generic useful tools, including non-overflowing math, signature verification, and trustless paying systems. + +The [full API](https://docs.openzeppelin.com/contracts/api/token/ERC20) is also thoroughly documented, and serves as a great reference when developing your smart contract application. You can also ask for help or follow Contracts's development in the [community forum](https://forum.openzeppelin.com). + +Finally, you may want to take a look at the [guides on our blog](https://blog.openzeppelin.com/guides), which cover several common use cases and good practices.. The following articles provide great background reading, though please note, some of the referenced tools have changed as the tooling in the ecosystem continues to rapidly evolve. + +- [The Hitchhiker’s Guide to Smart Contracts in Ethereum](https://blog.openzeppelin.com/the-hitchhikers-guide-to-smart-contracts-in-ethereum-848f08001f05) will help you get an overview of the various tools available for smart contract development, and help you set up your environment. +- [A Gentle Introduction to Ethereum Programming, Part 1](https://blog.openzeppelin.com/a-gentle-introduction-to-ethereum-programming-part-1-783cc7796094) provides very useful information on an introductory level, including many basic concepts from the Ethereum platform. +- For a more in-depth dive, you may read the guide [Designing the Architecture for Your Ethereum Application](https://blog.openzeppelin.com/designing-the-architecture-for-your-ethereum-application-9cec086f8317), which discusses how to better structure your application and its relationship to the real world. + +## Security + +This project is maintained by [OpenZeppelin](https://openzeppelin.com), and developed following our high standards for code quality and security. OpenZeppelin is meant to provide tested and community-audited code, but please use common sense when doing anything that deals with real money! We take no responsibility for your implementation decisions and any security problems you might experience. + +The core development principles and strategies that OpenZeppelin is based on include: security in depth, simple and modular code, clarity-driven naming conventions, comprehensive unit testing, pre-and-post-condition sanity checks, code consistency, and regular audits. + +The latest audit was done on October 2018 on version 2.0.0. + +Please report any security issues you find to security@openzeppelin.org. + +## Contribute + +OpenZeppelin exists thanks to its contributors. There are many ways you can participate and help build high quality software. Check out the [contribution guide](CONTRIBUTING.md)! + +## License + +OpenZeppelin is released under the [MIT License](LICENSE). diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol new file mode 100644 index 0000000..c11ed6d --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../utils/EnumerableSetUpgradeable.sol"; +import "../utils/AddressUpgradeable.sol"; +import "../GSN/ContextUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev Contract module that allows children to implement role-based access + * control mechanisms. + * + * Roles are referred to by their `bytes32` identifier. These should be exposed + * in the external API and be unique. The best way to achieve this is by + * using `public constant` hash digests: + * + * ``` + * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); + * ``` + * + * Roles can be used to represent a set of permissions. To restrict access to a + * function call, use {hasRole}: + * + * ``` + * function foo() public { + * require(hasRole(MY_ROLE, msg.sender)); + * ... + * } + * ``` + * + * Roles can be granted and revoked dynamically via the {grantRole} and + * {revokeRole} functions. Each role has an associated admin role, and only + * accounts that have a role's admin role can call {grantRole} and {revokeRole}. + * + * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means + * that only accounts with this role will be able to grant or revoke other + * roles. More complex role relationships can be created by using + * {_setRoleAdmin}. + * + * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to + * grant and revoke this role. Extra precautions should be taken to secure + * accounts that have been granted it. + */ +abstract contract AccessControlUpgradeable is + Initializable, + ContextUpgradeable +{ + function __AccessControl_init() internal initializer { + __Context_init_unchained(); + __AccessControl_init_unchained(); + } + + function __AccessControl_init_unchained() internal initializer {} + + using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; + using AddressUpgradeable for address; + + struct RoleData { + EnumerableSetUpgradeable.AddressSet members; + bytes32 adminRole; + } + + mapping(bytes32 => RoleData) private _roles; + + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + /** + * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` + * + * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite + * {RoleAdminChanged} not being emitted signaling this. + * + * _Available since v3.1._ + */ + event RoleAdminChanged( + bytes32 indexed role, + bytes32 indexed previousAdminRole, + bytes32 indexed newAdminRole + ); + + /** + * @dev Emitted when `account` is granted `role`. + * + * `sender` is the account that originated the contract call, an admin role + * bearer except when using {_setupRole}. + */ + event RoleGranted( + bytes32 indexed role, + address indexed account, + address indexed sender + ); + + /** + * @dev Emitted when `account` is revoked `role`. + * + * `sender` is the account that originated the contract call: + * - if using `revokeRole`, it is the admin role bearer + * - if using `renounceRole`, it is the role bearer (i.e. `account`) + */ + event RoleRevoked( + bytes32 indexed role, + address indexed account, + address indexed sender + ); + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) public view returns (bool) { + return _roles[role].members.contains(account); + } + + /** + * @dev Returns the number of accounts that have `role`. Can be used + * together with {getRoleMember} to enumerate all bearers of a role. + */ + function getRoleMemberCount(bytes32 role) public view returns (uint256) { + return _roles[role].members.length(); + } + + /** + * @dev Returns one of the accounts that have `role`. `index` must be a + * value between 0 and {getRoleMemberCount}, non-inclusive. + * + * Role bearers are not sorted in any particular way, and their ordering may + * change at any point. + * + * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure + * you perform all queries on the same block. See the following + * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] + * for more information. + */ + function getRoleMember(bytes32 role, uint256 index) + public + view + returns (address) + { + return _roles[role].members.at(index); + } + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) public view returns (bytes32) { + return _roles[role].adminRole; + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function grantRole(bytes32 role, address account) public virtual { + require( + hasRole(_roles[role].adminRole, _msgSender()), + "AccessControl: sender must be an admin to grant" + ); + + _grantRole(role, account); + } + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function revokeRole(bytes32 role, address account) public virtual { + require( + hasRole(_roles[role].adminRole, _msgSender()), + "AccessControl: sender must be an admin to revoke" + ); + + _revokeRole(role, account); + } + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been granted `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `account`. + */ + function renounceRole(bytes32 role, address account) public virtual { + require( + account == _msgSender(), + "AccessControl: can only renounce roles for self" + ); + + _revokeRole(role, account); + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. Note that unlike {grantRole}, this function doesn't perform any + * checks on the calling account. + * + * [WARNING] + * ==== + * This function should only be called from the constructor when setting + * up the initial roles for the system. + * + * Using this function in any other way is effectively circumventing the admin + * system imposed by {AccessControl}. + * ==== + */ + function _setupRole(bytes32 role, address account) internal virtual { + _grantRole(role, account); + } + + /** + * @dev Sets `adminRole` as ``role``'s admin role. + * + * Emits a {RoleAdminChanged} event. + */ + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { + emit RoleAdminChanged(role, _roles[role].adminRole, adminRole); + _roles[role].adminRole = adminRole; + } + + function _grantRole(bytes32 role, address account) private { + if (_roles[role].members.add(account)) { + emit RoleGranted(role, account, _msgSender()); + } + } + + function _revokeRole(bytes32 role, address account) private { + if (_roles[role].members.remove(account)) { + emit RoleRevoked(role, account, _msgSender()); + } + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol new file mode 100644 index 0000000..570ed81 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../GSN/ContextUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +contract OwnableUpgradeable is Initializable, ContextUpgradeable { + address private _owner; + + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + function __Ownable_init() internal initializer { + __Context_init_unchained(); + __Ownable_init_unchained(); + } + + function __Ownable_init_unchained() internal initializer { + address msgSender = _msgSender(); + _owner = msgSender; + emit OwnershipTransferred(address(0), msgSender); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view returns (address) { + return _owner; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(_owner == _msgSender(), "Ownable: caller is not the owner"); + _; + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + emit OwnershipTransferred(_owner, address(0)); + _owner = address(0); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current 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; + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/access/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/access/README.adoc new file mode 100644 index 0000000..42e6584 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/access/README.adoc @@ -0,0 +1,12 @@ += Access + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/access + +Contract modules for authorization and access control mechanisms. + +== Contracts + +{{Ownable}} + +{{AccessControl}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/cryptography/ECDSAUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/cryptography/ECDSAUpgradeable.sol new file mode 100644 index 0000000..8865411 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/cryptography/ECDSAUpgradeable.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSAUpgradeable { + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) + internal + pure + returns (address) + { + // Check the signature length + if (signature.length != 65) { + revert("ECDSA: invalid signature length"); + } + + // Divide the signature in r, s and v variables + bytes32 r; + bytes32 s; + uint8 v; + + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + // solhint-disable-next-line no-inline-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if ( + uint256(s) > + 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 + ) { + revert("ECDSA: invalid signature 's' value"); + } + + if (v != 27 && v != 28) { + revert("ECDSA: invalid signature 'v' value"); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + require(signer != address(0), "ECDSA: invalid signature"); + + return signer; + } + + /** + * @dev Returns an Ethereum Signed Message, created from a `hash`. This + * replicates the behavior of the + * https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`] + * JSON-RPC method. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes32 hash) + internal + pure + returns (bytes32) + { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return + keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", hash) + ); + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/cryptography/MerkleProofUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/cryptography/MerkleProofUpgradeable.sol new file mode 100644 index 0000000..a485f41 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/cryptography/MerkleProofUpgradeable.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev These functions deal with verification of Merkle trees (hash trees), + */ +library MerkleProofUpgradeable { + /** + * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree + * defined by `root`. For this, a `proof` must be provided, containing + * sibling hashes on the branch from the leaf to the root of the tree. Each + * pair of leaves and each pair of pre-images are assumed to be sorted. + */ + function verify( + bytes32[] memory proof, + bytes32 root, + bytes32 leaf + ) internal pure returns (bool) { + bytes32 computedHash = leaf; + + for (uint256 i = 0; i < proof.length; i++) { + bytes32 proofElement = proof[i]; + + if (computedHash <= proofElement) { + // Hash(current computed hash + current element of the proof) + computedHash = keccak256( + abi.encodePacked(computedHash, proofElement) + ); + } else { + // Hash(current element of the proof + current computed hash) + computedHash = keccak256( + abi.encodePacked(proofElement, computedHash) + ); + } + } + + // Check if the computed hash (root) is equal to the provided root + return computedHash == root; + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/cryptography/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/cryptography/README.adoc new file mode 100644 index 0000000..996e9da --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/cryptography/README.adoc @@ -0,0 +1,12 @@ += Cryptography + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/cryptography + +This collection of libraries provides simple and safe ways to use different cryptographic primitives. + +== Libraries + +{{ECDSA}} + +{{MerkleProof}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/ERC165CheckerUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/ERC165CheckerUpgradeable.sol new file mode 100644 index 0000000..1e1f228 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/ERC165CheckerUpgradeable.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.2; + +/** + * @dev Library used to query support of an interface declared via {IERC165}. + * + * Note that these functions return the actual result of the query: they do not + * `revert` if an interface is not supported. It is up to the caller to decide + * what to do in these cases. + */ +library ERC165CheckerUpgradeable { + // As per the EIP-165 spec, no interface should ever match 0xffffffff + bytes4 private constant _INTERFACE_ID_INVALID = 0xffffffff; + + /* + * bytes4(keccak256('supportsInterface(bytes4)')) == 0x01ffc9a7 + */ + bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7; + + /** + * @dev Returns true if `account` supports the {IERC165} interface, + */ + function supportsERC165(address account) internal view returns (bool) { + // Any contract that implements ERC165 must explicitly indicate support of + // InterfaceId_ERC165 and explicitly indicate non-support of InterfaceId_Invalid + return + _supportsERC165Interface(account, _INTERFACE_ID_ERC165) && + !_supportsERC165Interface(account, _INTERFACE_ID_INVALID); + } + + /** + * @dev Returns true if `account` supports the interface defined by + * `interfaceId`. Support for {IERC165} itself is queried automatically. + * + * See {IERC165-supportsInterface}. + */ + function supportsInterface(address account, bytes4 interfaceId) + internal + view + returns (bool) + { + // query support of both ERC165 as per the spec and support of _interfaceId + return + supportsERC165(account) && + _supportsERC165Interface(account, interfaceId); + } + + /** + * @dev Returns true if `account` supports all the interfaces defined in + * `interfaceIds`. Support for {IERC165} itself is queried automatically. + * + * Batch-querying can lead to gas savings by skipping repeated checks for + * {IERC165} support. + * + * See {IERC165-supportsInterface}. + */ + function supportsAllInterfaces( + address account, + bytes4[] memory interfaceIds + ) internal view returns (bool) { + // query support of ERC165 itself + if (!supportsERC165(account)) { + return false; + } + + // query support of each interface in _interfaceIds + for (uint256 i = 0; i < interfaceIds.length; i++) { + if (!_supportsERC165Interface(account, interfaceIds[i])) { + return false; + } + } + + // all interfaces supported + return true; + } + + /** + * @notice Query if a contract implements an interface, does not check ERC165 support + * @param account The address of the contract to query for support of an interface + * @param interfaceId The interface identifier, as specified in ERC-165 + * @return true if the contract at account indicates support of the interface with + * identifier interfaceId, false otherwise + * @dev Assumes that account contains a contract that supports ERC165, otherwise + * the behavior of this method is undefined. This precondition can be checked + * with {supportsERC165}. + * Interface identification is specified in ERC-165. + */ + function _supportsERC165Interface(address account, bytes4 interfaceId) + private + view + returns (bool) + { + // success determines whether the staticcall succeeded and result determines + // whether the contract at account indicates support of _interfaceId + (bool success, bool result) = _callERC165SupportsInterface( + account, + interfaceId + ); + + return (success && result); + } + + /** + * @notice Calls the function with selector 0x01ffc9a7 (ERC165) and suppresses throw + * @param account The address of the contract to query for support of an interface + * @param interfaceId The interface identifier, as specified in ERC-165 + * @return success true if the STATICCALL succeeded, false otherwise + * @return result true if the STATICCALL succeeded and the contract at account + * indicates support of the interface with identifier interfaceId, false otherwise + */ + function _callERC165SupportsInterface(address account, bytes4 interfaceId) + private + view + returns (bool, bool) + { + bytes memory encodedParams = abi.encodeWithSelector( + _INTERFACE_ID_ERC165, + interfaceId + ); + (bool success, bytes memory result) = account.staticcall{gas: 30000}( + encodedParams + ); + if (result.length < 32) return (false, false); + return (success, abi.decode(result, (bool))); + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/ERC165Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/ERC165Upgradeable.sol new file mode 100644 index 0000000..b2de939 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/ERC165Upgradeable.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./IERC165Upgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev Implementation of the {IERC165} interface. + * + * Contracts may inherit from this and call {_registerInterface} to declare + * their support of an interface. + */ +contract ERC165Upgradeable is Initializable, IERC165Upgradeable { + /* + * bytes4(keccak256('supportsInterface(bytes4)')) == 0x01ffc9a7 + */ + bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7; + + /** + * @dev Mapping of interface ids to whether or not it's supported. + */ + mapping(bytes4 => bool) private _supportedInterfaces; + + function __ERC165_init() internal initializer { + __ERC165_init_unchained(); + } + + function __ERC165_init_unchained() internal initializer { + // Derived contracts need only register support for their own interfaces, + // we register support for ERC165 itself here + _registerInterface(_INTERFACE_ID_ERC165); + } + + /** + * @dev See {IERC165-supportsInterface}. + * + * Time complexity O(1), guaranteed to always use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) + public + override + view + returns (bool) + { + return _supportedInterfaces[interfaceId]; + } + + /** + * @dev Registers the contract as an implementer of the interface defined by + * `interfaceId`. Support of the actual ERC165 interface is automatic and + * registering its interface id is not required. + * + * See {IERC165-supportsInterface}. + * + * Requirements: + * + * - `interfaceId` cannot be the ERC165 invalid interface (`0xffffffff`). + */ + function _registerInterface(bytes4 interfaceId) internal virtual { + require(interfaceId != 0xffffffff, "ERC165: invalid interface id"); + _supportedInterfaces[interfaceId] = true; + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/ERC1820ImplementerUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/ERC1820ImplementerUpgradeable.sol new file mode 100644 index 0000000..4fa5c61 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/ERC1820ImplementerUpgradeable.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./IERC1820ImplementerUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev Implementation of the {IERC1820Implementer} interface. + * + * Contracts may inherit from this and call {_registerInterfaceForAddress} to + * declare their willingness to be implementers. + * {IERC1820Registry-setInterfaceImplementer} should then be called for the + * registration to be complete. + */ +contract ERC1820ImplementerUpgradeable is + Initializable, + IERC1820ImplementerUpgradeable +{ + function __ERC1820Implementer_init() internal initializer { + __ERC1820Implementer_init_unchained(); + } + + function __ERC1820Implementer_init_unchained() internal initializer {} + + bytes32 private constant _ERC1820_ACCEPT_MAGIC = keccak256( + abi.encodePacked("ERC1820_ACCEPT_MAGIC") + ); + + mapping(bytes32 => mapping(address => bool)) private _supportedInterfaces; + + /** + * See {IERC1820Implementer-canImplementInterfaceForAddress}. + */ + function canImplementInterfaceForAddress( + bytes32 interfaceHash, + address account + ) public override view returns (bytes32) { + return + _supportedInterfaces[interfaceHash][account] + ? _ERC1820_ACCEPT_MAGIC + : bytes32(0x00); + } + + /** + * @dev Declares the contract as willing to be an implementer of + * `interfaceHash` for `account`. + * + * See {IERC1820Registry-setInterfaceImplementer} and + * {IERC1820Registry-interfaceHash}. + */ + function _registerInterfaceForAddress( + bytes32 interfaceHash, + address account + ) internal virtual { + _supportedInterfaces[interfaceHash][account] = true; + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/IERC165Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/IERC165Upgradeable.sol new file mode 100644 index 0000000..4b60f57 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/IERC165Upgradeable.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165Upgradeable { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/IERC1820ImplementerUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/IERC1820ImplementerUpgradeable.sol new file mode 100644 index 0000000..e27a41a --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/IERC1820ImplementerUpgradeable.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Interface for an ERC1820 implementer, as defined in the + * https://eips.ethereum.org/EIPS/eip-1820#interface-implementation-erc1820implementerinterface[EIP]. + * Used by contracts that will be registered as implementers in the + * {IERC1820Registry}. + */ +interface IERC1820ImplementerUpgradeable { + /** + * @dev Returns a special value (`ERC1820_ACCEPT_MAGIC`) if this contract + * implements `interfaceHash` for `account`. + * + * See {IERC1820Registry-setInterfaceImplementer}. + */ + function canImplementInterfaceForAddress( + bytes32 interfaceHash, + address account + ) external view returns (bytes32); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/IERC1820RegistryUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/IERC1820RegistryUpgradeable.sol new file mode 100644 index 0000000..ef025ff --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/IERC1820RegistryUpgradeable.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Interface of the global ERC1820 Registry, as defined in the + * https://eips.ethereum.org/EIPS/eip-1820[EIP]. Accounts may register + * implementers for interfaces in this registry, as well as query support. + * + * Implementers may be shared by multiple accounts, and can also implement more + * than a single interface for each account. Contracts can implement interfaces + * for themselves, but externally-owned accounts (EOA) must delegate this to a + * contract. + * + * {IERC165} interfaces can also be queried via the registry. + * + * For an in-depth explanation and source code analysis, see the EIP text. + */ +interface IERC1820RegistryUpgradeable { + /** + * @dev Sets `newManager` as the manager for `account`. A manager of an + * account is able to set interface implementers for it. + * + * By default, each account is its own manager. Passing a value of `0x0` in + * `newManager` will reset the manager to this initial state. + * + * Emits a {ManagerChanged} event. + * + * Requirements: + * + * - the caller must be the current manager for `account`. + */ + function setManager(address account, address newManager) external; + + /** + * @dev Returns the manager for `account`. + * + * See {setManager}. + */ + function getManager(address account) external view returns (address); + + /** + * @dev Sets the `implementer` contract as ``account``'s implementer for + * `interfaceHash`. + * + * `account` being the zero address is an alias for the caller's address. + * The zero address can also be used in `implementer` to remove an old one. + * + * See {interfaceHash} to learn how these are created. + * + * Emits an {InterfaceImplementerSet} event. + * + * Requirements: + * + * - the caller must be the current manager for `account`. + * - `interfaceHash` must not be an {IERC165} interface id (i.e. it must not + * end in 28 zeroes). + * - `implementer` must implement {IERC1820Implementer} and return true when + * queried for support, unless `implementer` is the caller. See + * {IERC1820Implementer-canImplementInterfaceForAddress}. + */ + function setInterfaceImplementer( + address account, + bytes32 interfaceHash, + address implementer + ) external; + + /** + * @dev Returns the implementer of `interfaceHash` for `account`. If no such + * implementer is registered, returns the zero address. + * + * If `interfaceHash` is an {IERC165} interface id (i.e. it ends with 28 + * zeroes), `account` will be queried for support of it. + * + * `account` being the zero address is an alias for the caller's address. + */ + function getInterfaceImplementer(address account, bytes32 interfaceHash) + external + view + returns (address); + + /** + * @dev Returns the interface hash for an `interfaceName`, as defined in the + * corresponding + * https://eips.ethereum.org/EIPS/eip-1820#interface-name[section of the EIP]. + */ + function interfaceHash(string calldata interfaceName) + external + pure + returns (bytes32); + + /** + * @notice Updates the cache with whether the contract implements an ERC165 interface or not. + * @param account Address of the contract for which to update the cache. + * @param interfaceId ERC165 interface for which to update the cache. + */ + function updateERC165Cache(address account, bytes4 interfaceId) external; + + /** + * @notice Checks whether a contract implements an ERC165 interface or not. + * If the result is not cached a direct lookup on the contract address is performed. + * If the result is not cached or the cached value is out-of-date, the cache MUST be updated manually by calling + * {updateERC165Cache} with the contract address. + * @param account Address of the contract to check. + * @param interfaceId ERC165 interface to check. + * @return True if `account` implements `interfaceId`, false otherwise. + */ + function implementsERC165Interface(address account, bytes4 interfaceId) + external + view + returns (bool); + + /** + * @notice Checks whether a contract implements an ERC165 interface or not without using nor updating the cache. + * @param account Address of the contract to check. + * @param interfaceId ERC165 interface to check. + * @return True if `account` implements `interfaceId`, false otherwise. + */ + function implementsERC165InterfaceNoCache( + address account, + bytes4 interfaceId + ) external view returns (bool); + + event InterfaceImplementerSet( + address indexed account, + bytes32 indexed interfaceHash, + address indexed implementer + ); + + event ManagerChanged(address indexed account, address indexed newManager); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/README.adoc new file mode 100644 index 0000000..38bd782 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/introspection/README.adoc @@ -0,0 +1,31 @@ += Introspection + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/introspection + +This set of interfaces and contracts deal with https://en.wikipedia.org/wiki/Type_introspection[type introspection] of contracts, that is, examining which functions can be called on them. This is usually referred to as a contract's _interface_. + +Ethereum contracts have no native concept of an interface, so applications must usually simply trust they are not making an incorrect call. For trusted setups this is a non-issue, but often unknown and untrusted third-party addresses need to be interacted with. There may even not be any direct calls to them! (e.g. `ERC20` tokens may be sent to a contract that lacks a way to transfer them out of it, locking them forever). In these cases, a contract _declaring_ its interface can be very helpful in preventing errors. + +There are two main ways to approach this. + +* Locally, where a contract implements `IERC165` and declares an interface, and a second one queries it directly via `ERC165Checker`. +* Globally, where a global and unique registry (`IERC1820Registry`) is used to register implementers of a certain interface (`IERC1820Implementer`). It is then the registry that is queried, which allows for more complex setups, like contracts implementing interfaces for externally-owned accounts. + +Note that, in all cases, accounts simply _declare_ their interfaces, but they are not required to actually implement them. This mechanism can therefore be used to both prevent errors and allow for complex interactions (see `ERC777`), but it must not be relied on for security. + +== Local + +{{IERC165}} + +{{ERC165}} + +{{ERC165Checker}} + +== Global + +{{IERC1820Registry}} + +{{IERC1820Implementer}} + +{{ERC1820Implementer}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/MathUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/MathUpgradeable.sol new file mode 100644 index 0000000..954c060 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/MathUpgradeable.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library MathUpgradeable { + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a >= b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow, so we distribute + return (a / 2) + (b / 2) + (((a % 2) + (b % 2)) / 2); + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/README.adoc new file mode 100644 index 0000000..b03d441 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/README.adoc @@ -0,0 +1,14 @@ += Math + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/math + +These are math-related utilities. + +== Libraries + +{{SafeMath}} + +{{SignedSafeMath}} + +{{Math}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol new file mode 100644 index 0000000..c134d81 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMathUpgradeable { + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/SignedSafeMathUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/SignedSafeMathUpgradeable.sol new file mode 100644 index 0000000..8003249 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/math/SignedSafeMathUpgradeable.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @title SignedSafeMath + * @dev Signed math operations with safety checks that revert on error. + */ +library SignedSafeMathUpgradeable { + int256 private constant _INT256_MIN = -2**255; + + /** + * @dev Returns the multiplication of two signed integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * + * - Multiplication cannot overflow. + */ + function mul(int256 a, int256 b) internal pure returns (int256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + require( + !(a == -1 && b == _INT256_MIN), + "SignedSafeMath: multiplication overflow" + ); + + int256 c = a * b; + require(c / a == b, "SignedSafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two signed integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(int256 a, int256 b) internal pure returns (int256) { + require(b != 0, "SignedSafeMath: division by zero"); + require( + !(b == -1 && a == _INT256_MIN), + "SignedSafeMath: division overflow" + ); + + int256 c = a / b; + + return c; + } + + /** + * @dev Returns the subtraction of two signed integers, reverting on + * overflow. + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(int256 a, int256 b) internal pure returns (int256) { + int256 c = a - b; + require( + (b >= 0 && c <= a) || (b < 0 && c > a), + "SignedSafeMath: subtraction overflow" + ); + + return c; + } + + /** + * @dev Returns the addition of two signed integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * + * - Addition cannot overflow. + */ + function add(int256 a, int256 b) internal pure returns (int256) { + int256 c = a + b; + require( + (b >= 0 && c >= a) || (b < 0 && c < a), + "SignedSafeMath: addition overflow" + ); + + return c; + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/package.json b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/package.json new file mode 100644 index 0000000..33e5994 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/package.json @@ -0,0 +1,63 @@ +{ + "_from": "@openzeppelin/contracts-upgradeable", + "_id": "@openzeppelin/contracts-upgradeable@3.2.0", + "_inBundle": false, + "_integrity": "sha512-f2q4FjT84ixIl8MUo9WbCMSBtV9T52c8QbrCfzoCBbDwEuVjf9jnvm1JETAWSC9Q9oLcUXpRFBMavb1B23tx1g==", + "_location": "/@openzeppelin/contracts-upgradeable", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "@openzeppelin/contracts-upgradeable", + "name": "@openzeppelin/contracts-upgradeable", + "escapedName": "@openzeppelin%2fcontracts-upgradeable", + "scope": "@openzeppelin", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-3.2.0.tgz", + "_shasum": "fb6959659e7585f419060ea1c546c0c11f1d0582", + "_spec": "@openzeppelin/contracts-upgradeable", + "_where": "/Users/present/code/brownie-sett", + "author": { + "name": "OpenZeppelin Community", + "email": "maintainers@openzeppelin.org" + }, + "bugs": { + "url": "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Secure Smart Contract library for Solidity", + "files": [ + "**/*.sol", + "/build/contracts/*.json", + "!/mocks", + "!/examples" + ], + "homepage": "https://openzeppelin.com/contracts/", + "keywords": [ + "solidity", + "ethereum", + "smart", + "contracts", + "security", + "zeppelin" + ], + "license": "MIT", + "name": "@openzeppelin/contracts-upgradeable", + "repository": { + "type": "git", + "url": "git+https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git" + }, + "scripts": { + "prepare": "bash ../scripts/prepare-contracts-package.sh", + "prepare-docs": "cd ..; npm run prepare-docs" + }, + "version": "3.2.0" +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/PaymentSplitterUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/PaymentSplitterUpgradeable.sol new file mode 100644 index 0000000..cff24b1 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/PaymentSplitterUpgradeable.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../GSN/ContextUpgradeable.sol"; +import "../math/SafeMathUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @title PaymentSplitter + * @dev This contract allows to split Ether payments among a group of accounts. The sender does not need to be aware + * that the Ether will be split in this way, since it is handled transparently by the contract. + * + * The split can be in equal parts or in any other arbitrary proportion. The way this is specified is by assigning each + * account to a number of shares. Of all the Ether that this contract receives, each account will then be able to claim + * an amount proportional to the percentage of total shares they were assigned. + * + * `PaymentSplitter` follows a _pull payment_ model. This means that payments are not automatically forwarded to the + * accounts but kept in this contract, and the actual transfer is triggered as a separate step by calling the {release} + * function. + */ +contract PaymentSplitterUpgradeable is Initializable, ContextUpgradeable { + using SafeMathUpgradeable for uint256; + + event PayeeAdded(address account, uint256 shares); + event PaymentReleased(address to, uint256 amount); + event PaymentReceived(address from, uint256 amount); + + uint256 private _totalShares; + uint256 private _totalReleased; + + mapping(address => uint256) private _shares; + mapping(address => uint256) private _released; + address[] private _payees; + + /** + * @dev Creates an instance of `PaymentSplitter` where each account in `payees` is assigned the number of shares at + * the matching position in the `shares` array. + * + * All addresses in `payees` must be non-zero. Both arrays must have the same non-zero length, and there must be no + * duplicates in `payees`. + */ + function __PaymentSplitter_init( + address[] memory payees, + uint256[] memory shares + ) internal initializer { + __Context_init_unchained(); + __PaymentSplitter_init_unchained(payees, shares); + } + + function __PaymentSplitter_init_unchained( + address[] memory payees, + uint256[] memory shares + ) internal initializer { + // solhint-disable-next-line max-line-length + require( + payees.length == shares.length, + "PaymentSplitter: payees and shares length mismatch" + ); + require(payees.length > 0, "PaymentSplitter: no payees"); + + for (uint256 i = 0; i < payees.length; i++) { + _addPayee(payees[i], shares[i]); + } + } + + /** + * @dev The Ether received will be logged with {PaymentReceived} events. Note that these events are not fully + * reliable: it's possible for a contract to receive Ether without triggering this function. This only affects the + * reliability of the events, and not the actual splitting of Ether. + * + * To learn more about this see the Solidity documentation for + * https://solidity.readthedocs.io/en/latest/contracts.html#fallback-function[fallback + * functions]. + */ + receive() external payable { + emit PaymentReceived(_msgSender(), msg.value); + } + + /** + * @dev Getter for the total shares held by payees. + */ + function totalShares() public view returns (uint256) { + return _totalShares; + } + + /** + * @dev Getter for the total amount of Ether already released. + */ + function totalReleased() public view returns (uint256) { + return _totalReleased; + } + + /** + * @dev Getter for the amount of shares held by an account. + */ + function shares(address account) public view returns (uint256) { + return _shares[account]; + } + + /** + * @dev Getter for the amount of Ether already released to a payee. + */ + function released(address account) public view returns (uint256) { + return _released[account]; + } + + /** + * @dev Getter for the address of the payee number `index`. + */ + function payee(uint256 index) public view returns (address) { + return _payees[index]; + } + + /** + * @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the + * total shares and their previous withdrawals. + */ + function release(address payable account) public virtual { + require(_shares[account] > 0, "PaymentSplitter: account has no shares"); + + uint256 totalReceived = address(this).balance.add(_totalReleased); + uint256 payment = totalReceived + .mul(_shares[account]) + .div(_totalShares) + .sub(_released[account]); + + require(payment != 0, "PaymentSplitter: account is not due payment"); + + _released[account] = _released[account].add(payment); + _totalReleased = _totalReleased.add(payment); + + account.transfer(payment); + emit PaymentReleased(account, payment); + } + + /** + * @dev Add a new payee to the contract. + * @param account The address of the payee to add. + * @param shares_ The number of shares owned by the payee. + */ + function _addPayee(address account, uint256 shares_) private { + require( + account != address(0), + "PaymentSplitter: account is the zero address" + ); + require(shares_ > 0, "PaymentSplitter: shares are 0"); + require( + _shares[account] == 0, + "PaymentSplitter: account already has shares" + ); + + _payees.push(account); + _shares[account] = shares_; + _totalShares = _totalShares.add(shares_); + emit PayeeAdded(account, shares_); + } + + uint256[45] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/PullPaymentUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/PullPaymentUpgradeable.sol new file mode 100644 index 0000000..74310a9 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/PullPaymentUpgradeable.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.2; + +import "./escrow/EscrowUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev Simple implementation of a + * https://consensys.github.io/smart-contract-best-practices/recommendations/#favor-pull-over-push-for-external-calls[pull-payment] + * strategy, where the paying contract doesn't interact directly with the + * receiver account, which must withdraw its payments itself. + * + * Pull-payments are often considered the best practice when it comes to sending + * Ether, security-wise. It prevents recipients from blocking execution, and + * eliminates reentrancy concerns. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + * + * To use, derive from the `PullPayment` contract, and use {_asyncTransfer} + * instead of Solidity's `transfer` function. Payees can query their due + * payments with {payments}, and retrieve them with {withdrawPayments}. + */ +contract PullPaymentUpgradeable is Initializable { + EscrowUpgradeable private _escrow; + + function __PullPayment_init() internal initializer { + __PullPayment_init_unchained(); + } + + function __PullPayment_init_unchained() internal initializer { + _escrow = new EscrowUpgradeable(); + _escrow.initialize(); + } + + /** + * @dev Withdraw accumulated payments, forwarding all gas to the recipient. + * + * Note that _any_ account can call this function, not just the `payee`. + * This means that contracts unaware of the `PullPayment` protocol can still + * receive funds this way, by having a separate account call + * {withdrawPayments}. + * + * WARNING: Forwarding all gas opens the door to reentrancy vulnerabilities. + * Make sure you trust the recipient, or are either following the + * checks-effects-interactions pattern or using {ReentrancyGuard}. + * + * @param payee Whose payments will be withdrawn. + */ + function withdrawPayments(address payable payee) public virtual { + _escrow.withdraw(payee); + } + + /** + * @dev Returns the payments owed to an address. + * @param dest The creditor's address. + */ + function payments(address dest) public view returns (uint256) { + return _escrow.depositsOf(dest); + } + + /** + * @dev Called by the payer to store the sent amount as credit to be pulled. + * Funds sent in this way are stored in an intermediate {Escrow} contract, so + * there is no danger of them being spent before withdrawal. + * + * @param dest The destination address of the funds. + * @param amount The amount to transfer. + */ + function _asyncTransfer(address dest, uint256 amount) internal virtual { + _escrow.deposit{value: amount}(dest); + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/README.adoc new file mode 100644 index 0000000..7d6151d --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/README.adoc @@ -0,0 +1,22 @@ += Payment + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/payment + +Utilities related to sending and receiving payments. Examples are {PullPayment}, which implements the best security practices when sending funds to third parties, and {PaymentSplitter} to receive incoming payments among a number of beneficiaries. + +TIP: When transferring funds to and from untrusted third parties, there is always a security risk of reentrancy. If you would like to learn more about this and ways to protect against it, check out our blog post https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + +== Utilities + +{{PaymentSplitter}} + +{{PullPayment}} + +== Escrow + +{{Escrow}} + +{{ConditionalEscrow}} + +{{RefundEscrow}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/escrow/ConditionalEscrowUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/escrow/ConditionalEscrowUpgradeable.sol new file mode 100644 index 0000000..b24707a --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/escrow/ConditionalEscrowUpgradeable.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./EscrowUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @title ConditionalEscrow + * @dev Base abstract escrow to only allow withdrawal if a condition is met. + * @dev Intended usage: See {Escrow}. Same usage guidelines apply here. + */ +abstract contract ConditionalEscrowUpgradeable is + Initializable, + EscrowUpgradeable +{ + function __ConditionalEscrow_init() internal initializer { + __Context_init_unchained(); + __Ownable_init_unchained(); + __Escrow_init_unchained(); + __ConditionalEscrow_init_unchained(); + } + + function __ConditionalEscrow_init_unchained() internal initializer {} + + /** + * @dev Returns whether an address is allowed to withdraw their funds. To be + * implemented by derived contracts. + * @param payee The destination address of the funds. + */ + function withdrawalAllowed(address payee) + public + virtual + view + returns (bool); + + function withdraw(address payable payee) public virtual override { + require( + withdrawalAllowed(payee), + "ConditionalEscrow: payee is not allowed to withdraw" + ); + super.withdraw(payee); + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/escrow/EscrowUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/escrow/EscrowUpgradeable.sol new file mode 100644 index 0000000..e25ee23 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/escrow/EscrowUpgradeable.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../../math/SafeMathUpgradeable.sol"; +import "../../access/OwnableUpgradeable.sol"; +import "../../utils/AddressUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @title Escrow + * @dev Base escrow contract, holds funds designated for a payee until they + * withdraw them. + * + * Intended usage: This contract (and derived escrow contracts) should be a + * standalone contract, that only interacts with the contract that instantiated + * it. That way, it is guaranteed that all Ether will be handled according to + * the `Escrow` rules, and there is no need to check for payable functions or + * transfers in the inheritance tree. The contract that uses the escrow as its + * payment method should be its owner, and provide public methods redirecting + * to the escrow's deposit and withdraw. + */ +contract EscrowUpgradeable is Initializable, OwnableUpgradeable { + function initialize() public virtual initializer { + __Escrow_init(); + } + + function __Escrow_init() internal initializer { + __Context_init_unchained(); + __Ownable_init_unchained(); + __Escrow_init_unchained(); + } + + function __Escrow_init_unchained() internal initializer {} + + using SafeMathUpgradeable for uint256; + using AddressUpgradeable for address payable; + + event Deposited(address indexed payee, uint256 weiAmount); + event Withdrawn(address indexed payee, uint256 weiAmount); + + mapping(address => uint256) private _deposits; + + function depositsOf(address payee) public view returns (uint256) { + return _deposits[payee]; + } + + /** + * @dev Stores the sent amount as credit to be withdrawn. + * @param payee The destination address of the funds. + */ + function deposit(address payee) public virtual payable onlyOwner { + uint256 amount = msg.value; + _deposits[payee] = _deposits[payee].add(amount); + + emit Deposited(payee, amount); + } + + /** + * @dev Withdraw accumulated balance for a payee, forwarding all gas to the + * recipient. + * + * WARNING: Forwarding all gas opens the door to reentrancy vulnerabilities. + * Make sure you trust the recipient, or are either following the + * checks-effects-interactions pattern or using {ReentrancyGuard}. + * + * @param payee The address whose funds will be withdrawn and transferred to. + */ + function withdraw(address payable payee) public virtual onlyOwner { + uint256 payment = _deposits[payee]; + + _deposits[payee] = 0; + + payee.sendValue(payment); + + emit Withdrawn(payee, payment); + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/escrow/RefundEscrowUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/escrow/RefundEscrowUpgradeable.sol new file mode 100644 index 0000000..827e5bb --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/payment/escrow/RefundEscrowUpgradeable.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./ConditionalEscrowUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @title RefundEscrow + * @dev Escrow that holds funds for a beneficiary, deposited from multiple + * parties. + * @dev Intended usage: See {Escrow}. Same usage guidelines apply here. + * @dev The owner account (that is, the contract that instantiates this + * contract) may deposit, close the deposit period, and allow for either + * withdrawal by the beneficiary, or refunds to the depositors. All interactions + * with `RefundEscrow` will be made through the owner contract. + */ +contract RefundEscrowUpgradeable is + Initializable, + ConditionalEscrowUpgradeable +{ + enum State {Active, Refunding, Closed} + + event RefundsClosed(); + event RefundsEnabled(); + + State private _state; + address payable private _beneficiary; + + /** + * @dev Constructor. + * @param beneficiary The beneficiary of the deposits. + */ + function __RefundEscrow_init(address payable beneficiary) + internal + initializer + { + __Context_init_unchained(); + __Ownable_init_unchained(); + __Escrow_init_unchained(); + __ConditionalEscrow_init_unchained(); + __RefundEscrow_init_unchained(beneficiary); + } + + function __RefundEscrow_init_unchained(address payable beneficiary) + internal + initializer + { + require( + beneficiary != address(0), + "RefundEscrow: beneficiary is the zero address" + ); + _beneficiary = beneficiary; + _state = State.Active; + } + + /** + * @return The current state of the escrow. + */ + function state() public view returns (State) { + return _state; + } + + /** + * @return The beneficiary of the escrow. + */ + function beneficiary() public view returns (address) { + return _beneficiary; + } + + /** + * @dev Stores funds that may later be refunded. + * @param refundee The address funds will be sent to if a refund occurs. + */ + function deposit(address refundee) public virtual override payable { + require( + _state == State.Active, + "RefundEscrow: can only deposit while active" + ); + super.deposit(refundee); + } + + /** + * @dev Allows for the beneficiary to withdraw their funds, rejecting + * further deposits. + */ + function close() public virtual onlyOwner { + require( + _state == State.Active, + "RefundEscrow: can only close while active" + ); + _state = State.Closed; + emit RefundsClosed(); + } + + /** + * @dev Allows for refunds to take place, rejecting further deposits. + */ + function enableRefunds() public virtual onlyOwner { + require( + _state == State.Active, + "RefundEscrow: can only enable refunds while active" + ); + _state = State.Refunding; + emit RefundsEnabled(); + } + + /** + * @dev Withdraws the beneficiary's funds. + */ + function beneficiaryWithdraw() public virtual { + require( + _state == State.Closed, + "RefundEscrow: beneficiary can only withdraw while closed" + ); + _beneficiary.transfer(address(this).balance); + } + + /** + * @dev Returns whether refundees can withdraw their deposits (be refunded). The overridden function receives a + * 'payee' argument, but we ignore it here since the condition is global, not per-payee. + */ + function withdrawalAllowed(address) public override view returns (bool) { + return _state == State.Refunding; + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/ERC1155PresetMinterPauserUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/ERC1155PresetMinterPauserUpgradeable.sol new file mode 100644 index 0000000..fdd37e3 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/ERC1155PresetMinterPauserUpgradeable.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../access/AccessControlUpgradeable.sol"; +import "../GSN/ContextUpgradeable.sol"; +import "../token/ERC1155/ERC1155Upgradeable.sol"; +import "../token/ERC1155/ERC1155BurnableUpgradeable.sol"; +import "../token/ERC1155/ERC1155PausableUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev {ERC1155} token, including: + * + * - ability for holders to burn (destroy) their tokens + * - a minter role that allows for token minting (creation) + * - a pauser role that allows to stop all token transfers + * + * This contract uses {AccessControl} to lock permissioned functions using the + * different roles - head to its documentation for details. + * + * The account that deploys the contract will be granted the minter and pauser + * roles, as well as the default admin role, which will let it grant both minter + * and pauser roles to other accounts. + */ +contract ERC1155PresetMinterPauserUpgradeable is + Initializable, + ContextUpgradeable, + AccessControlUpgradeable, + ERC1155BurnableUpgradeable, + ERC1155PausableUpgradeable +{ + function initialize(string memory uri) public virtual initializer { + __ERC1155PresetMinterPauser_init(uri); + } + + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /** + * @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE`, and `PAUSER_ROLE` to the account that + * deploys the contract. + */ + function __ERC1155PresetMinterPauser_init(string memory uri) + internal + initializer + { + __Context_init_unchained(); + __AccessControl_init_unchained(); + __ERC165_init_unchained(); + __ERC1155_init_unchained(uri); + __ERC1155Burnable_init_unchained(); + __Pausable_init_unchained(); + __ERC1155Pausable_init_unchained(); + __ERC1155PresetMinterPauser_init_unchained(uri); + } + + function __ERC1155PresetMinterPauser_init_unchained(string memory uri) + internal + initializer + { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + + _setupRole(MINTER_ROLE, _msgSender()); + _setupRole(PAUSER_ROLE, _msgSender()); + } + + /** + * @dev Creates `amount` new tokens for `to`, of token type `id`. + * + * See {ERC1155-_mint}. + * + * Requirements: + * + * - the caller must have the `MINTER_ROLE`. + */ + function mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual { + require( + hasRole(MINTER_ROLE, _msgSender()), + "ERC1155PresetMinterPauser: must have minter role to mint" + ); + + _mint(to, id, amount, data); + } + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] variant of {mint}. + */ + function mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual { + require( + hasRole(MINTER_ROLE, _msgSender()), + "ERC1155PresetMinterPauser: must have minter role to mint" + ); + + _mintBatch(to, ids, amounts, data); + } + + /** + * @dev Pauses all token transfers. + * + * See {ERC1155Pausable} and {Pausable-_pause}. + * + * Requirements: + * + * - the caller must have the `PAUSER_ROLE`. + */ + function pause() public virtual { + require( + hasRole(PAUSER_ROLE, _msgSender()), + "ERC1155PresetMinterPauser: must have pauser role to pause" + ); + _pause(); + } + + /** + * @dev Unpauses all token transfers. + * + * See {ERC1155Pausable} and {Pausable-_unpause}. + * + * Requirements: + * + * - the caller must have the `PAUSER_ROLE`. + */ + function unpause() public virtual { + require( + hasRole(PAUSER_ROLE, _msgSender()), + "ERC1155PresetMinterPauser: must have pauser role to unpause" + ); + _unpause(); + } + + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) + internal + virtual + override(ERC1155Upgradeable, ERC1155PausableUpgradeable) + { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/ERC20PresetMinterPauserUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/ERC20PresetMinterPauserUpgradeable.sol new file mode 100644 index 0000000..f59bf39 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/ERC20PresetMinterPauserUpgradeable.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../access/AccessControlUpgradeable.sol"; +import "../GSN/ContextUpgradeable.sol"; +import "../token/ERC20/ERC20Upgradeable.sol"; +import "../token/ERC20/ERC20BurnableUpgradeable.sol"; +import "../token/ERC20/ERC20PausableUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev {ERC20} token, including: + * + * - ability for holders to burn (destroy) their tokens + * - a minter role that allows for token minting (creation) + * - a pauser role that allows to stop all token transfers + * + * This contract uses {AccessControl} to lock permissioned functions using the + * different roles - head to its documentation for details. + * + * The account that deploys the contract will be granted the minter and pauser + * roles, as well as the default admin role, which will let it grant both minter + * and pauser roles to other accounts. + */ +contract ERC20PresetMinterPauserUpgradeable is + Initializable, + ContextUpgradeable, + AccessControlUpgradeable, + ERC20BurnableUpgradeable, + ERC20PausableUpgradeable +{ + function initialize(string memory name, string memory symbol) + public + virtual + initializer + { + __ERC20PresetMinterPauser_init(name, symbol); + } + + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /** + * @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE` and `PAUSER_ROLE` to the + * account that deploys the contract. + * + * See {ERC20-constructor}. + */ + function __ERC20PresetMinterPauser_init( + string memory name, + string memory symbol + ) internal initializer { + __Context_init_unchained(); + __AccessControl_init_unchained(); + __ERC20_init_unchained(name, symbol); + __ERC20Burnable_init_unchained(); + __Pausable_init_unchained(); + __ERC20Pausable_init_unchained(); + __ERC20PresetMinterPauser_init_unchained(name, symbol); + } + + function __ERC20PresetMinterPauser_init_unchained( + string memory name, + string memory symbol + ) internal initializer { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + + _setupRole(MINTER_ROLE, _msgSender()); + _setupRole(PAUSER_ROLE, _msgSender()); + } + + /** + * @dev Creates `amount` new tokens for `to`. + * + * See {ERC20-_mint}. + * + * Requirements: + * + * - the caller must have the `MINTER_ROLE`. + */ + function mint(address to, uint256 amount) public virtual { + require( + hasRole(MINTER_ROLE, _msgSender()), + "ERC20PresetMinterPauser: must have minter role to mint" + ); + _mint(to, amount); + } + + /** + * @dev Pauses all token transfers. + * + * See {ERC20Pausable} and {Pausable-_pause}. + * + * Requirements: + * + * - the caller must have the `PAUSER_ROLE`. + */ + function pause() public virtual { + require( + hasRole(PAUSER_ROLE, _msgSender()), + "ERC20PresetMinterPauser: must have pauser role to pause" + ); + _pause(); + } + + /** + * @dev Unpauses all token transfers. + * + * See {ERC20Pausable} and {Pausable-_unpause}. + * + * Requirements: + * + * - the caller must have the `PAUSER_ROLE`. + */ + function unpause() public virtual { + require( + hasRole(PAUSER_ROLE, _msgSender()), + "ERC20PresetMinterPauser: must have pauser role to unpause" + ); + _unpause(); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override(ERC20Upgradeable, ERC20PausableUpgradeable) { + super._beforeTokenTransfer(from, to, amount); + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/ERC721PresetMinterPauserAutoIdUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/ERC721PresetMinterPauserAutoIdUpgradeable.sol new file mode 100644 index 0000000..c77e44a --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/ERC721PresetMinterPauserAutoIdUpgradeable.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../access/AccessControlUpgradeable.sol"; +import "../GSN/ContextUpgradeable.sol"; +import "../utils/CountersUpgradeable.sol"; +import "../token/ERC721/ERC721Upgradeable.sol"; +import "../token/ERC721/ERC721BurnableUpgradeable.sol"; +import "../token/ERC721/ERC721PausableUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev {ERC721} token, including: + * + * - ability for holders to burn (destroy) their tokens + * - a minter role that allows for token minting (creation) + * - a pauser role that allows to stop all token transfers + * - token ID and URI autogeneration + * + * This contract uses {AccessControl} to lock permissioned functions using the + * different roles - head to its documentation for details. + * + * The account that deploys the contract will be granted the minter and pauser + * roles, as well as the default admin role, which will let it grant both minter + * and pauser roles to other accounts. + */ +contract ERC721PresetMinterPauserAutoIdUpgradeable is + Initializable, + ContextUpgradeable, + AccessControlUpgradeable, + ERC721BurnableUpgradeable, + ERC721PausableUpgradeable +{ + function initialize( + string memory name, + string memory symbol, + string memory baseURI + ) public virtual initializer { + __ERC721PresetMinterPauserAutoId_init(name, symbol, baseURI); + } + + using CountersUpgradeable for CountersUpgradeable.Counter; + + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + CountersUpgradeable.Counter private _tokenIdTracker; + + /** + * @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE` and `PAUSER_ROLE` to the + * account that deploys the contract. + * + * Token URIs will be autogenerated based on `baseURI` and their token IDs. + * See {ERC721-tokenURI}. + */ + function __ERC721PresetMinterPauserAutoId_init( + string memory name, + string memory symbol, + string memory baseURI + ) internal initializer { + __Context_init_unchained(); + __AccessControl_init_unchained(); + __ERC165_init_unchained(); + __ERC721_init_unchained(name, symbol); + __ERC721Burnable_init_unchained(); + __Pausable_init_unchained(); + __ERC721Pausable_init_unchained(); + __ERC721PresetMinterPauserAutoId_init_unchained(name, symbol, baseURI); + } + + function __ERC721PresetMinterPauserAutoId_init_unchained( + string memory name, + string memory symbol, + string memory baseURI + ) internal initializer { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + + _setupRole(MINTER_ROLE, _msgSender()); + _setupRole(PAUSER_ROLE, _msgSender()); + + _setBaseURI(baseURI); + } + + /** + * @dev Creates a new token for `to`. Its token ID will be automatically + * assigned (and available on the emitted {IERC721-Transfer} event), and the token + * URI autogenerated based on the base URI passed at construction. + * + * See {ERC721-_mint}. + * + * Requirements: + * + * - the caller must have the `MINTER_ROLE`. + */ + function mint(address to) public virtual { + require( + hasRole(MINTER_ROLE, _msgSender()), + "ERC721PresetMinterPauserAutoId: must have minter role to mint" + ); + + // We cannot just use balanceOf to create the new tokenId because tokens + // can be burned (destroyed), so we need a separate counter. + _mint(to, _tokenIdTracker.current()); + _tokenIdTracker.increment(); + } + + /** + * @dev Pauses all token transfers. + * + * See {ERC721Pausable} and {Pausable-_pause}. + * + * Requirements: + * + * - the caller must have the `PAUSER_ROLE`. + */ + function pause() public virtual { + require( + hasRole(PAUSER_ROLE, _msgSender()), + "ERC721PresetMinterPauserAutoId: must have pauser role to pause" + ); + _pause(); + } + + /** + * @dev Unpauses all token transfers. + * + * See {ERC721Pausable} and {Pausable-_unpause}. + * + * Requirements: + * + * - the caller must have the `PAUSER_ROLE`. + */ + function unpause() public virtual { + require( + hasRole(PAUSER_ROLE, _msgSender()), + "ERC721PresetMinterPauserAutoId: must have pauser role to unpause" + ); + _unpause(); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override(ERC721Upgradeable, ERC721PausableUpgradeable) { + super._beforeTokenTransfer(from, to, tokenId); + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/README.adoc new file mode 100644 index 0000000..df29419 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/presets/README.adoc @@ -0,0 +1,18 @@ += Presets + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/presets + +These contracts integrate different Ethereum standards (ERCs) with custom extensions and modules, showcasing common configurations that are ready to deploy **without having to write any Solidity code**. + +They can be used as-is for quick prototyping and testing, but are **also suitable for production environments**. + +TIP: Intermediate and advanced users can use these as starting points when writing their own contracts, extending them with custom functionality as they see fit. + +== Tokens + +{{ERC20PresetMinterPauser}} + +{{ERC721PresetMinterPauserAutoId}} + +{{ERC1155PresetMinterPauser}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/proxy/Initializable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/proxy/Initializable.sol new file mode 100644 index 0000000..a2a1d11 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/proxy/Initializable.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.4.24 <0.7.0; + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since a proxied contract can't have a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {UpgradeableProxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + */ +abstract contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + */ + bool private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Modifier to protect an initializer function from being invoked twice. + */ + modifier initializer() { + require( + _initializing || _isConstructor() || !_initialized, + "Initializable: contract is already initialized" + ); + + bool isTopLevelCall = !_initializing; + if (isTopLevelCall) { + _initializing = true; + _initialized = true; + } + + _; + + if (isTopLevelCall) { + _initializing = false; + } + } + + /// @dev Returns true if and only if the function is running in the constructor + function _isConstructor() private view returns (bool) { + // extcodesize checks the size of the code stored in an address, and + // address returns the current address. Since the code is still not + // deployed when running a constructor, any checks on its code size will + // yield zero, making it an effective way to detect if a contract is + // under construction or not. + address self = address(this); + uint256 cs; + // solhint-disable-next-line no-inline-assembly + assembly { + cs := extcodesize(self) + } + return cs == 0; + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/proxy/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/proxy/README.adoc new file mode 100644 index 0000000..68d5585 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/proxy/README.adoc @@ -0,0 +1,26 @@ += Proxies + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/proxy + +This is a low-level set of contracts implementing the proxy pattern for upgradeability. For an in-depth overview of this pattern check out the xref:upgrades-plugins::proxies.adoc[Proxy Upgrade Pattern] page. + +The abstract {Proxy} contract implements the core delegation functionality. If the concrete proxies that we provide below are not suitable, we encourage building on top of this base contract since it contains an assembly block that may be hard to get right. + +Upgradeability is implemented in the {UpgradeableProxy} contract, although it provides only an internal upgrade interface. For an upgrade interface exposed externally to an admin, we provide {TransparentUpgradeableProxy}. Both of these contracts use the storage slots specified in https://eips.ethereum.org/EIPS/eip-1967[EIP1967] to avoid clashes with the storage of the implementation contract behind the proxy. + +CAUTION: Using upgradeable proxies correctly and securely is a difficult task that requires deep knowledge of the proxy pattern, Solidity, and the EVM. Unless you want a lot of low level control, we recommend using the xref:upgrades-plugins::index.adoc[OpenZeppelin Upgrades Plugins] for Truffle and Buidler. + +== Core + +{{Proxy}} + +{{UpgradeableProxy}} + +{{TransparentUpgradeableProxy}} + +== Utilities + +{{Initializable}} + +{{ProxyAdmin}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155BurnableUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155BurnableUpgradeable.sol new file mode 100644 index 0000000..a903080 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155BurnableUpgradeable.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./ERC1155Upgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev Extension of {ERC1155} that allows token holders to destroy both their + * own tokens and those that they have been approved to use. + * + * _Available since v3.1._ + */ +abstract contract ERC1155BurnableUpgradeable is + Initializable, + ERC1155Upgradeable +{ + function __ERC1155Burnable_init() internal initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __ERC1155Burnable_init_unchained(); + } + + function __ERC1155Burnable_init_unchained() internal initializer {} + + function burn( + address account, + uint256 id, + uint256 value + ) public virtual { + require( + account == _msgSender() || isApprovedForAll(account, _msgSender()), + "ERC1155: caller is not owner nor approved" + ); + + _burn(account, id, value); + } + + function burnBatch( + address account, + uint256[] memory ids, + uint256[] memory values + ) public virtual { + require( + account == _msgSender() || isApprovedForAll(account, _msgSender()), + "ERC1155: caller is not owner nor approved" + ); + + _burnBatch(account, ids, values); + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155HolderUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155HolderUpgradeable.sol new file mode 100644 index 0000000..99f157a --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155HolderUpgradeable.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./ERC1155ReceiverUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev _Available since v3.1._ + */ +contract ERC1155HolderUpgradeable is Initializable, ERC1155ReceiverUpgradeable { + function __ERC1155Holder_init() internal initializer { + __ERC165_init_unchained(); + __ERC1155Receiver_init_unchained(); + __ERC1155Holder_init_unchained(); + } + + function __ERC1155Holder_init_unchained() internal initializer {} + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155PausableUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155PausableUpgradeable.sol new file mode 100644 index 0000000..0ec7839 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155PausableUpgradeable.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./ERC1155Upgradeable.sol"; +import "../../utils/PausableUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev ERC1155 token with pausable token transfers, minting and burning. + * + * Useful for scenarios such as preventing trades until the end of an evaluation + * period, or having an emergency switch for freezing all token transfers in the + * event of a large bug. + * + * _Available since v3.1._ + */ +abstract contract ERC1155PausableUpgradeable is + Initializable, + ERC1155Upgradeable, + PausableUpgradeable +{ + function __ERC1155Pausable_init() internal initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __Pausable_init_unchained(); + __ERC1155Pausable_init_unchained(); + } + + function __ERC1155Pausable_init_unchained() internal initializer {} + + /** + * @dev See {ERC1155-_beforeTokenTransfer}. + * + * Requirements: + * + * - the contract must not be paused. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + + require(!paused(), "ERC1155Pausable: token transfer while paused"); + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155ReceiverUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155ReceiverUpgradeable.sol new file mode 100644 index 0000000..b0317bd --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155ReceiverUpgradeable.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./IERC1155ReceiverUpgradeable.sol"; +import "../../introspection/ERC165Upgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev _Available since v3.1._ + */ +abstract contract ERC1155ReceiverUpgradeable is + Initializable, + ERC165Upgradeable, + IERC1155ReceiverUpgradeable +{ + function __ERC1155Receiver_init() internal initializer { + __ERC165_init_unchained(); + __ERC1155Receiver_init_unchained(); + } + + function __ERC1155Receiver_init_unchained() internal initializer { + _registerInterface( + ERC1155ReceiverUpgradeable(0).onERC1155Received.selector ^ + ERC1155ReceiverUpgradeable(0).onERC1155BatchReceived.selector + ); + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol new file mode 100644 index 0000000..10475e6 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol @@ -0,0 +1,537 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./IERC1155Upgradeable.sol"; +import "./IERC1155MetadataURIUpgradeable.sol"; +import "./IERC1155ReceiverUpgradeable.sol"; +import "../../GSN/ContextUpgradeable.sol"; +import "../../introspection/ERC165Upgradeable.sol"; +import "../../math/SafeMathUpgradeable.sol"; +import "../../utils/AddressUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * + * @dev Implementation of the basic standard multi-token. + * See https://eips.ethereum.org/EIPS/eip-1155 + * Originally based on code by Enjin: https://github.com/enjin/erc-1155 + * + * _Available since v3.1._ + */ +contract ERC1155Upgradeable is + Initializable, + ContextUpgradeable, + ERC165Upgradeable, + IERC1155Upgradeable, + IERC1155MetadataURIUpgradeable +{ + using SafeMathUpgradeable for uint256; + using AddressUpgradeable for address; + + // Mapping from token ID to account balances + mapping(uint256 => mapping(address => uint256)) private _balances; + + // Mapping from account to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + // Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json + string private _uri; + + /* + * bytes4(keccak256('balanceOf(address,uint256)')) == 0x00fdd58e + * bytes4(keccak256('balanceOfBatch(address[],uint256[])')) == 0x4e1273f4 + * bytes4(keccak256('setApprovalForAll(address,bool)')) == 0xa22cb465 + * bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c5 + * bytes4(keccak256('safeTransferFrom(address,address,uint256,uint256,bytes)')) == 0xf242432a + * bytes4(keccak256('safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)')) == 0x2eb2c2d6 + * + * => 0x00fdd58e ^ 0x4e1273f4 ^ 0xa22cb465 ^ + * 0xe985e9c5 ^ 0xf242432a ^ 0x2eb2c2d6 == 0xd9b67a26 + */ + bytes4 private constant _INTERFACE_ID_ERC1155 = 0xd9b67a26; + + /* + * bytes4(keccak256('uri(uint256)')) == 0x0e89341c + */ + bytes4 private constant _INTERFACE_ID_ERC1155_METADATA_URI = 0x0e89341c; + + /** + * @dev See {_setURI}. + */ + function __ERC1155_init(string memory uri) internal initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __ERC1155_init_unchained(uri); + } + + function __ERC1155_init_unchained(string memory uri) internal initializer { + _setURI(uri); + + // register the supported interfaces to conform to ERC1155 via ERC165 + _registerInterface(_INTERFACE_ID_ERC1155); + + // register the supported interfaces to conform to ERC1155MetadataURI via ERC165 + _registerInterface(_INTERFACE_ID_ERC1155_METADATA_URI); + } + + /** + * @dev See {IERC1155MetadataURI-uri}. + * + * This implementation returns the same URI for *all* token types. It relies + * on the token type ID substitution mechanism + * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. + * + * Clients calling this function must replace the `\{id\}` substring with the + * actual token type ID. + */ + function uri(uint256) external override view returns (string memory) { + return _uri; + } + + /** + * @dev See {IERC1155-balanceOf}. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function balanceOf(address account, uint256 id) + public + override + view + returns (uint256) + { + require( + account != address(0), + "ERC1155: balance query for the zero address" + ); + return _balances[id][account]; + } + + /** + * @dev See {IERC1155-balanceOfBatch}. + * + * Requirements: + * + * - `accounts` and `ids` must have the same length. + */ + function balanceOfBatch(address[] memory accounts, uint256[] memory ids) + public + override + view + returns (uint256[] memory) + { + require( + accounts.length == ids.length, + "ERC1155: accounts and ids length mismatch" + ); + + uint256[] memory batchBalances = new uint256[](accounts.length); + + for (uint256 i = 0; i < accounts.length; ++i) { + require( + accounts[i] != address(0), + "ERC1155: batch balance query for the zero address" + ); + batchBalances[i] = _balances[ids[i]][accounts[i]]; + } + + return batchBalances; + } + + /** + * @dev See {IERC1155-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) + public + virtual + override + { + require( + _msgSender() != operator, + "ERC1155: setting approval status for self" + ); + + _operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC1155-isApprovedForAll}. + */ + function isApprovedForAll(address account, address operator) + public + override + view + returns (bool) + { + return _operatorApprovals[account][operator]; + } + + /** + * @dev See {IERC1155-safeTransferFrom}. + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual override { + require(to != address(0), "ERC1155: transfer to the zero address"); + require( + from == _msgSender() || isApprovedForAll(from, _msgSender()), + "ERC1155: caller is not owner nor approved" + ); + + address operator = _msgSender(); + + _beforeTokenTransfer( + operator, + from, + to, + _asSingletonArray(id), + _asSingletonArray(amount), + data + ); + + _balances[id][from] = _balances[id][from].sub( + amount, + "ERC1155: insufficient balance for transfer" + ); + _balances[id][to] = _balances[id][to].add(amount); + + emit TransferSingle(operator, from, to, id, amount); + + _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); + } + + /** + * @dev See {IERC1155-safeBatchTransferFrom}. + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual override { + require( + ids.length == amounts.length, + "ERC1155: ids and amounts length mismatch" + ); + require(to != address(0), "ERC1155: transfer to the zero address"); + require( + from == _msgSender() || isApprovedForAll(from, _msgSender()), + "ERC1155: transfer caller is not owner nor approved" + ); + + address operator = _msgSender(); + + _beforeTokenTransfer(operator, from, to, ids, amounts, data); + + for (uint256 i = 0; i < ids.length; ++i) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + _balances[id][from] = _balances[id][from].sub( + amount, + "ERC1155: insufficient balance for transfer" + ); + _balances[id][to] = _balances[id][to].add(amount); + } + + emit TransferBatch(operator, from, to, ids, amounts); + + _doSafeBatchTransferAcceptanceCheck( + operator, + from, + to, + ids, + amounts, + data + ); + } + + /** + * @dev Sets a new URI for all token types, by relying on the token type ID + * substitution mechanism + * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. + * + * By this mechanism, any occurrence of the `\{id\}` substring in either the + * URI or any of the amounts in the JSON file at said URI will be replaced by + * clients with the token type ID. + * + * For example, the `https://token-cdn-domain/\{id\}.json` URI would be + * interpreted by clients as + * `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` + * for token type ID 0x4cce0. + * + * See {uri}. + * + * Because these URIs cannot be meaningfully represented by the {URI} event, + * this function emits no events. + */ + function _setURI(string memory newuri) internal virtual { + _uri = newuri; + } + + /** + * @dev Creates `amount` tokens of token type `id`, and assigns them to `account`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function _mint( + address account, + uint256 id, + uint256 amount, + bytes memory data + ) internal virtual { + require(account != address(0), "ERC1155: mint to the zero address"); + + address operator = _msgSender(); + + _beforeTokenTransfer( + operator, + address(0), + account, + _asSingletonArray(id), + _asSingletonArray(amount), + data + ); + + _balances[id][account] = _balances[id][account].add(amount); + emit TransferSingle(operator, address(0), account, id, amount); + + _doSafeTransferAcceptanceCheck( + operator, + address(0), + account, + id, + amount, + data + ); + } + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_mint}. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function _mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual { + require(to != address(0), "ERC1155: mint to the zero address"); + require( + ids.length == amounts.length, + "ERC1155: ids and amounts length mismatch" + ); + + address operator = _msgSender(); + + _beforeTokenTransfer(operator, address(0), to, ids, amounts, data); + + for (uint256 i = 0; i < ids.length; i++) { + _balances[ids[i]][to] = amounts[i].add(_balances[ids[i]][to]); + } + + emit TransferBatch(operator, address(0), to, ids, amounts); + + _doSafeBatchTransferAcceptanceCheck( + operator, + address(0), + to, + ids, + amounts, + data + ); + } + + /** + * @dev Destroys `amount` tokens of token type `id` from `account` + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens of token type `id`. + */ + function _burn( + address account, + uint256 id, + uint256 amount + ) internal virtual { + require(account != address(0), "ERC1155: burn from the zero address"); + + address operator = _msgSender(); + + _beforeTokenTransfer( + operator, + account, + address(0), + _asSingletonArray(id), + _asSingletonArray(amount), + "" + ); + + _balances[id][account] = _balances[id][account].sub( + amount, + "ERC1155: burn amount exceeds balance" + ); + + emit TransferSingle(operator, account, address(0), id, amount); + } + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_burn}. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + */ + function _burnBatch( + address account, + uint256[] memory ids, + uint256[] memory amounts + ) internal virtual { + require(account != address(0), "ERC1155: burn from the zero address"); + require( + ids.length == amounts.length, + "ERC1155: ids and amounts length mismatch" + ); + + address operator = _msgSender(); + + _beforeTokenTransfer(operator, account, address(0), ids, amounts, ""); + + for (uint256 i = 0; i < ids.length; i++) { + _balances[ids[i]][account] = _balances[ids[i]][account].sub( + amounts[i], + "ERC1155: burn amount exceeds balance" + ); + } + + emit TransferBatch(operator, account, address(0), ids, amounts); + } + + /** + * @dev Hook that is called before any token transfer. This includes minting + * and burning, as well as batched variants. + * + * The same hook is called on both single and batched variants. For single + * transfers, the length of the `id` and `amount` arrays will be 1. + * + * Calling conditions (for each `id` and `amount` pair): + * + * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * of token type `id` will be transferred to `to`. + * - When `from` is zero, `amount` tokens of token type `id` will be minted + * for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens of token type `id` + * will be burned. + * - `from` and `to` are never both zero. + * - `ids` and `amounts` have the same, non-zero length. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual {} + + function _doSafeTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) private { + if (to.isContract()) { + try + IERC1155ReceiverUpgradeable(to).onERC1155Received( + operator, + from, + id, + amount, + data + ) + returns (bytes4 response) { + if ( + response != + IERC1155ReceiverUpgradeable(to).onERC1155Received.selector + ) { + revert("ERC1155: ERC1155Receiver rejected tokens"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("ERC1155: transfer to non ERC1155Receiver implementer"); + } + } + } + + function _doSafeBatchTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) private { + if (to.isContract()) { + try + IERC1155ReceiverUpgradeable(to).onERC1155BatchReceived( + operator, + from, + ids, + amounts, + data + ) + returns (bytes4 response) { + if ( + response != + IERC1155ReceiverUpgradeable(to) + .onERC1155BatchReceived + .selector + ) { + revert("ERC1155: ERC1155Receiver rejected tokens"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("ERC1155: transfer to non ERC1155Receiver implementer"); + } + } + } + + function _asSingletonArray(uint256 element) + private + pure + returns (uint256[] memory) + { + uint256[] memory array = new uint256[](1); + array[0] = element; + + return array; + } + + uint256[47] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155MetadataURIUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155MetadataURIUpgradeable.sol new file mode 100644 index 0000000..9ed3930 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155MetadataURIUpgradeable.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.2; + +import "./IERC1155Upgradeable.sol"; + +/** + * @dev Interface of the optional ERC1155MetadataExtension interface, as defined + * in the https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[EIP]. + * + * _Available since v3.1._ + */ +interface IERC1155MetadataURIUpgradeable is IERC1155Upgradeable { + /** + * @dev Returns the URI for token type `id`. + * + * If the `\{id\}` substring is present in the URI, it must be replaced by + * clients with the actual token type ID. + */ + function uri(uint256 id) external view returns (string memory); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155ReceiverUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155ReceiverUpgradeable.sol new file mode 100644 index 0000000..a29f354 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155ReceiverUpgradeable.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../../introspection/IERC165Upgradeable.sol"; + +/** + * _Available since v3.1._ + */ +interface IERC1155ReceiverUpgradeable is IERC165Upgradeable { + /** + @dev Handles the receipt of a single ERC1155 token type. This function is + called at the end of a `safeTransferFrom` after the balance has been updated. + To accept the transfer, this must return + `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + (i.e. 0xf23a6e61, or its own function selector). + @param operator The address which initiated the transfer (i.e. msg.sender) + @param from The address which previously owned the token + @param id The ID of the token being transferred + @param value The amount of tokens being transferred + @param data Additional data with no specified format + @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /** + @dev Handles the receipt of a multiple ERC1155 token types. This function + is called at the end of a `safeBatchTransferFrom` after the balances have + been updated. To accept the transfer(s), this must return + `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + (i.e. 0xbc197c81, or its own function selector). + @param operator The address which initiated the batch transfer (i.e. msg.sender) + @param from The address which previously owned the token + @param ids An array containing ids of each token being transferred (order and length must match values array) + @param values An array containing amounts of each token being transferred (order and length must match ids array) + @param data Additional data with no specified format + @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol new file mode 100644 index 0000000..59b4325 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.2; + +import "../../introspection/IERC165Upgradeable.sol"; + +/** + * @dev Required interface of an ERC1155 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1155[EIP]. + * + * _Available since v3.1._ + */ +interface IERC1155Upgradeable is IERC165Upgradeable { + /** + * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`. + */ + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 value + ); + + /** + * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all + * transfers. + */ + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] values + ); + + /** + * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to + * `approved`. + */ + event ApprovalForAll( + address indexed account, + address indexed operator, + bool approved + ); + + /** + * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI. + * + * If an {URI} event was emitted for `id`, the standard + * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value + * returned by {IERC1155MetadataURI-uri}. + */ + event URI(string value, uint256 indexed id); + + /** + * @dev Returns the amount of tokens of token type `id` owned by `account`. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function balanceOf(address account, uint256 id) + external + view + returns (uint256); + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. + * + * Requirements: + * + * - `accounts` and `ids` must have the same length. + */ + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) + external + view + returns (uint256[] memory); + + /** + * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, + * + * Emits an {ApprovalForAll} event. + * + * Requirements: + * + * - `operator` cannot be the caller. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns true if `operator` is approved to transfer ``account``'s tokens. + * + * See {setApprovalForAll}. + */ + function isApprovedForAll(address account, address operator) + external + view + returns (bool); + + /** + * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - If the caller is not `from`, it must be have been approved to spend ``from``'s tokens via {setApprovalForAll}. + * - `from` must have a balance of tokens of type `id` of at least `amount`. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes calldata data + ) external; + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/README.adoc new file mode 100644 index 0000000..e804746 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC1155/README.adoc @@ -0,0 +1,37 @@ += ERC 1155 + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc1155 + +This set of interfaces and contracts are all related to the https://eips.ethereum.org/EIPS/eip-1155[ERC1155 Multi Token Standard]. + +The EIP consists of three interfaces which fulfill different roles, found here as {IERC1155}, {IERC1155MetadataURI} and {IERC1155Receiver}. + +{ERC1155} implements the mandatory {IERC1155} interface, as well as the optional extension {IERC1155MetadataURI}, by relying on the substitution mechanism to use the same URI for all token types, dramatically reducing gas costs. + +Additionally there are multiple custom extensions, including: + +* designation of addresses that can pause token transfers for all users ({ERC1155Pausable}). +* destruction of own tokens ({ERC1155Burnable}). + +NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC1155 (such as <>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc1155.adoc#Presets[ERC1155 Presets] (such as {ERC1155PresetMinterPauser}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts. + +== Core + +{{IERC1155}} + +{{IERC1155MetadataURI}} + +{{ERC1155}} + +{{IERC1155Receiver}} + +== Extensions + +{{ERC1155Pausable}} + +{{ERC1155Burnable}} + +== Convenience + +{{ERC1155Holder}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol new file mode 100644 index 0000000..3fe04f1 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../../GSN/ContextUpgradeable.sol"; +import "./ERC20Upgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev Extension of {ERC20} that allows token holders to destroy both their own + * tokens and those that they have an allowance for, in a way that can be + * recognized off-chain (via event analysis). + */ +abstract contract ERC20BurnableUpgradeable is + Initializable, + ContextUpgradeable, + ERC20Upgradeable +{ + function __ERC20Burnable_init() internal initializer { + __Context_init_unchained(); + __ERC20Burnable_init_unchained(); + } + + function __ERC20Burnable_init_unchained() internal initializer {} + + /** + * @dev Destroys `amount` tokens from the caller. + * + * See {ERC20-_burn}. + */ + function burn(uint256 amount) public virtual { + _burn(_msgSender(), amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, deducting from the caller's + * allowance. + * + * See {ERC20-_burn} and {ERC20-allowance}. + * + * Requirements: + * + * - the caller must have allowance for ``accounts``'s tokens of at least + * `amount`. + */ + function burnFrom(address account, uint256 amount) public virtual { + uint256 decreasedAllowance = allowance(account, _msgSender()).sub( + amount, + "ERC20: burn amount exceeds allowance" + ); + + _approve(account, _msgSender(), decreasedAllowance); + _burn(account, amount); + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20CappedUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20CappedUpgradeable.sol new file mode 100644 index 0000000..515464a --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20CappedUpgradeable.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./ERC20Upgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev Extension of {ERC20} that adds a cap to the supply of tokens. + */ +abstract contract ERC20CappedUpgradeable is Initializable, ERC20Upgradeable { + uint256 private _cap; + + /** + * @dev Sets the value of the `cap`. This value is immutable, it can only be + * set once during construction. + */ + function __ERC20Capped_init(uint256 cap) internal initializer { + __Context_init_unchained(); + __ERC20Capped_init_unchained(cap); + } + + function __ERC20Capped_init_unchained(uint256 cap) internal initializer { + require(cap > 0, "ERC20Capped: cap is 0"); + _cap = cap; + } + + /** + * @dev Returns the cap on the token's total supply. + */ + function cap() public view returns (uint256) { + return _cap; + } + + /** + * @dev See {ERC20-_beforeTokenTransfer}. + * + * Requirements: + * + * - minted tokens must not cause the total supply to go over the cap. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override { + super._beforeTokenTransfer(from, to, amount); + + if (from == address(0)) { + // When minting tokens + require( + totalSupply().add(amount) <= _cap, + "ERC20Capped: cap exceeded" + ); + } + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20PausableUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20PausableUpgradeable.sol new file mode 100644 index 0000000..0d34aa5 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20PausableUpgradeable.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./ERC20Upgradeable.sol"; +import "../../utils/PausableUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev ERC20 token with pausable token transfers, minting and burning. + * + * Useful for scenarios such as preventing trades until the end of an evaluation + * period, or having an emergency switch for freezing all token transfers in the + * event of a large bug. + */ +abstract contract ERC20PausableUpgradeable is + Initializable, + ERC20Upgradeable, + PausableUpgradeable +{ + function __ERC20Pausable_init() internal initializer { + __Context_init_unchained(); + __Pausable_init_unchained(); + __ERC20Pausable_init_unchained(); + } + + function __ERC20Pausable_init_unchained() internal initializer {} + + /** + * @dev See {ERC20-_beforeTokenTransfer}. + * + * Requirements: + * + * - the contract must not be paused. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override { + super._beforeTokenTransfer(from, to, amount); + + require(!paused(), "ERC20Pausable: token transfer while paused"); + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20SnapshotUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20SnapshotUpgradeable.sol new file mode 100644 index 0000000..c8389df --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20SnapshotUpgradeable.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../../math/SafeMathUpgradeable.sol"; +import "../../utils/ArraysUpgradeable.sol"; +import "../../utils/CountersUpgradeable.sol"; +import "./ERC20Upgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev This contract extends an ERC20 token with a snapshot mechanism. When a snapshot is created, the balances and + * total supply at the time are recorded for later access. + * + * This can be used to safely create mechanisms based on token balances such as trustless dividends or weighted voting. + * In naive implementations it's possible to perform a "double spend" attack by reusing the same balance from different + * accounts. By using snapshots to calculate dividends or voting power, those attacks no longer apply. It can also be + * used to create an efficient ERC20 forking mechanism. + * + * Snapshots are created by the internal {_snapshot} function, which will emit the {Snapshot} event and return a + * snapshot id. To get the total supply at the time of a snapshot, call the function {totalSupplyAt} with the snapshot + * id. To get the balance of an account at the time of a snapshot, call the {balanceOfAt} function with the snapshot id + * and the account address. + * + * ==== Gas Costs + * + * Snapshots are efficient. Snapshot creation is _O(1)_. Retrieval of balances or total supply from a snapshot is _O(log + * n)_ in the number of snapshots that have been created, although _n_ for a specific account will generally be much + * smaller since identical balances in subsequent snapshots are stored as a single entry. + * + * There is a constant overhead for normal ERC20 transfers due to the additional snapshot bookkeeping. This overhead is + * only significant for the first transfer that immediately follows a snapshot for a particular account. Subsequent + * transfers will have normal cost until the next snapshot, and so on. + */ +abstract contract ERC20SnapshotUpgradeable is Initializable, ERC20Upgradeable { + function __ERC20Snapshot_init() internal initializer { + __Context_init_unchained(); + __ERC20Snapshot_init_unchained(); + } + + function __ERC20Snapshot_init_unchained() internal initializer {} + + // Inspired by Jordi Baylina's MiniMeToken to record historical balances: + // https://github.com/Giveth/minimd/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol + + using SafeMathUpgradeable for uint256; + using ArraysUpgradeable for uint256[]; + using CountersUpgradeable for CountersUpgradeable.Counter; + + // Snapshotted values have arrays of ids and the value corresponding to that id. These could be an array of a + // Snapshot struct, but that would impede usage of functions that work on an array. + struct Snapshots { + uint256[] ids; + uint256[] values; + } + + mapping(address => Snapshots) private _accountBalanceSnapshots; + Snapshots private _totalSupplySnapshots; + + // Snapshot ids increase monotonically, with the first value being 1. An id of 0 is invalid. + CountersUpgradeable.Counter private _currentSnapshotId; + + /** + * @dev Emitted by {_snapshot} when a snapshot identified by `id` is created. + */ + event Snapshot(uint256 id); + + /** + * @dev Creates a new snapshot and returns its snapshot id. + * + * Emits a {Snapshot} event that contains the same id. + * + * {_snapshot} is `internal` and you have to decide how to expose it externally. Its usage may be restricted to a + * set of accounts, for example using {AccessControl}, or it may be open to the public. + * + * [WARNING] + * ==== + * While an open way of calling {_snapshot} is required for certain trust minimization mechanisms such as forking, + * you must consider that it can potentially be used by attackers in two ways. + * + * First, it can be used to increase the cost of retrieval of values from snapshots, although it will grow + * logarithmically thus rendering this attack ineffective in the long term. Second, it can be used to target + * specific accounts and increase the cost of ERC20 transfers for them, in the ways specified in the Gas Costs + * section above. + * + * We haven't measured the actual numbers; if this is something you're interested in please reach out to us. + * ==== + */ + function _snapshot() internal virtual returns (uint256) { + _currentSnapshotId.increment(); + + uint256 currentId = _currentSnapshotId.current(); + emit Snapshot(currentId); + return currentId; + } + + /** + * @dev Retrieves the balance of `account` at the time `snapshotId` was created. + */ + function balanceOfAt(address account, uint256 snapshotId) + public + view + returns (uint256) + { + (bool snapshotted, uint256 value) = _valueAt( + snapshotId, + _accountBalanceSnapshots[account] + ); + + return snapshotted ? value : balanceOf(account); + } + + /** + * @dev Retrieves the total supply at the time `snapshotId` was created. + */ + function totalSupplyAt(uint256 snapshotId) public view returns (uint256) { + (bool snapshotted, uint256 value) = _valueAt( + snapshotId, + _totalSupplySnapshots + ); + + return snapshotted ? value : totalSupply(); + } + + // Update balance and/or total supply snapshots before the values are modified. This is implemented + // in the _beforeTokenTransfer hook, which is executed for _mint, _burn, and _transfer operations. + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override { + super._beforeTokenTransfer(from, to, amount); + + if (from == address(0)) { + // mint + _updateAccountSnapshot(to); + _updateTotalSupplySnapshot(); + } else if (to == address(0)) { + // burn + _updateAccountSnapshot(from); + _updateTotalSupplySnapshot(); + } else { + // transfer + _updateAccountSnapshot(from); + _updateAccountSnapshot(to); + } + } + + function _valueAt(uint256 snapshotId, Snapshots storage snapshots) + private + view + returns (bool, uint256) + { + require(snapshotId > 0, "ERC20Snapshot: id is 0"); + // solhint-disable-next-line max-line-length + require( + snapshotId <= _currentSnapshotId.current(), + "ERC20Snapshot: nonexistent id" + ); + + // When a valid snapshot is queried, there are three possibilities: + // a) The queried value was not modified after the snapshot was taken. Therefore, a snapshot entry was never + // created for this id, and all stored snapshot ids are smaller than the requested one. The value that corresponds + // to this id is the current one. + // b) The queried value was modified after the snapshot was taken. Therefore, there will be an entry with the + // requested id, and its value is the one to return. + // c) More snapshots were created after the requested one, and the queried value was later modified. There will be + // no entry for the requested id: the value that corresponds to it is that of the smallest snapshot id that is + // larger than the requested one. + // + // In summary, we need to find an element in an array, returning the index of the smallest value that is larger if + // it is not found, unless said value doesn't exist (e.g. when all values are smaller). Arrays.findUpperBound does + // exactly this. + + uint256 index = snapshots.ids.findUpperBound(snapshotId); + + if (index == snapshots.ids.length) { + return (false, 0); + } else { + return (true, snapshots.values[index]); + } + } + + function _updateAccountSnapshot(address account) private { + _updateSnapshot(_accountBalanceSnapshots[account], balanceOf(account)); + } + + function _updateTotalSupplySnapshot() private { + _updateSnapshot(_totalSupplySnapshots, totalSupply()); + } + + function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) + private + { + uint256 currentId = _currentSnapshotId.current(); + if (_lastSnapshotId(snapshots.ids) < currentId) { + snapshots.ids.push(currentId); + snapshots.values.push(currentValue); + } + } + + function _lastSnapshotId(uint256[] storage ids) + private + view + returns (uint256) + { + if (ids.length == 0) { + return 0; + } else { + return ids[ids.length - 1]; + } + } + + uint256[46] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol new file mode 100644 index 0000000..2720ae4 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../../GSN/ContextUpgradeable.sol"; +import "./IERC20Upgradeable.sol"; +import "../../math/SafeMathUpgradeable.sol"; +import "../../utils/AddressUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of returning `false` on failure. This behavior is nonetheless conventional + * and does not conflict with the expectations of ERC20 applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20Upgradeable is + Initializable, + ContextUpgradeable, + IERC20Upgradeable +{ + using SafeMathUpgradeable for uint256; + using AddressUpgradeable for address; + + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @dev Sets the values for {name} and {symbol}, initializes {decimals} with + * a default value of 18. + * + * To select a different value for {decimals}, use {_setupDecimals}. + * + * All three of these values are immutable: they can only be set once during + * construction. + */ + function __ERC20_init(string memory name, string memory symbol) + internal + initializer + { + __Context_init_unchained(); + __ERC20_init_unchained(name, symbol); + } + + function __ERC20_init_unchained(string memory name, string memory symbol) + internal + initializer + { + _name = name; + _symbol = symbol; + _decimals = 18; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is + * called. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view returns (uint8) { + return _decimals; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public override view returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public override view returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) + public + virtual + override + returns (bool) + { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) + public + virtual + override + view + returns (uint256) + { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) + public + virtual + override + returns (bool) + { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + _approve( + sender, + _msgSender(), + _allowances[sender][_msgSender()].sub( + amount, + "ERC20: transfer amount exceeds allowance" + ) + ); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) + public + virtual + returns (bool) + { + _approve( + _msgSender(), + spender, + _allowances[_msgSender()][spender].add(addedValue) + ); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) + public + virtual + returns (bool) + { + _approve( + _msgSender(), + spender, + _allowances[_msgSender()][spender].sub( + subtractedValue, + "ERC20: decreased allowance below zero" + ) + ); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub( + amount, + "ERC20: transfer amount exceeds balance" + ); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub( + amount, + "ERC20: burn amount exceeds balance" + ); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Sets {decimals} to a value other than the default one of 18. + * + * WARNING: This function should only be called from the constructor. Most + * applications that interact with token contracts will not expect + * {decimals} to ever change, and may work incorrectly if it does. + */ + function _setupDecimals(uint8 decimals_) internal { + _decimals = decimals_; + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be to transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + uint256[44] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol new file mode 100644 index 0000000..ab8c6d3 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20Upgradeable { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) + external + returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) + external + view + returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/README.adoc new file mode 100644 index 0000000..f47f94b --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/README.adoc @@ -0,0 +1,49 @@ += ERC 20 + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc20 + +This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-20[ERC20 Token Standard]. + +TIP: For an overview of ERC20 tokens and a walk through on how to create a token contract read our xref:ROOT:erc20.adoc[ERC20 guide]. + +There a few core contracts that implement the behavior specified in the EIP: + +* {IERC20}: the interface all ERC20 implementations should conform to. +* {ERC20}: the implementation of the ERC20 interface, including the <>, <> and <> optional standard extension to the base interface. + +Additionally there are multiple custom extensions, including: + +* designation of addresses that can pause token transfers for all users ({ERC20Pausable}). +* efficient storage of past token balances to be later queried at any point in time ({ERC20Snapshot}). +* destruction of own tokens ({ERC20Burnable}). +* enforcement of a cap to the total supply when minting tokens ({ERC20Capped}). + +Finally, there are some utilities to interact with ERC20 contracts in various ways. + +* {SafeERC20} is a wrapper around the interface that eliminates the need to handle boolean return values. +* {TokenTimelock} can hold tokens for a beneficiary until a specified time. + +NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC20 (such as <>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc20.adoc#Presets[ERC20 Presets] (such as {ERC20PresetMinterPauser}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts. + +== Core + +{{IERC20}} + +{{ERC20}} + +== Extensions + +{{ERC20Snapshot}} + +{{ERC20Pausable}} + +{{ERC20Burnable}} + +{{ERC20Capped}} + +== Utilities + +{{SafeERC20}} + +{{TokenTimelock}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol new file mode 100644 index 0000000..77e343c --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./IERC20Upgradeable.sol"; +import "../../math/SafeMathUpgradeable.sol"; +import "../../utils/AddressUpgradeable.sol"; + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20Upgradeable { + using SafeMathUpgradeable for uint256; + using AddressUpgradeable for address; + + function safeTransfer( + IERC20Upgradeable token, + address to, + uint256 value + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(token.transfer.selector, to, value) + ); + } + + function safeTransferFrom( + IERC20Upgradeable token, + address from, + address to, + uint256 value + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(token.transferFrom.selector, from, to, value) + ); + } + + /** + * @dev Deprecated. This function has issues similar to the ones found in + * {IERC20-approve}, and its usage is discouraged. + * + * Whenever possible, use {safeIncreaseAllowance} and + * {safeDecreaseAllowance} instead. + */ + function safeApprove( + IERC20Upgradeable token, + address spender, + uint256 value + ) internal { + // safeApprove should only be called when setting an initial allowance, + // or when resetting it to zero. To increase and decrease it, use + // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' + // solhint-disable-next-line max-line-length + require( + (value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + _callOptionalReturn( + token, + abi.encodeWithSelector(token.approve.selector, spender, value) + ); + } + + function safeIncreaseAllowance( + IERC20Upgradeable token, + address spender, + uint256 value + ) internal { + uint256 newAllowance = token.allowance(address(this), spender).add( + value + ); + _callOptionalReturn( + token, + abi.encodeWithSelector( + token.approve.selector, + spender, + newAllowance + ) + ); + } + + function safeDecreaseAllowance( + IERC20Upgradeable token, + address spender, + uint256 value + ) internal { + uint256 newAllowance = token.allowance(address(this), spender).sub( + value, + "SafeERC20: decreased allowance below zero" + ); + _callOptionalReturn( + token, + abi.encodeWithSelector( + token.approve.selector, + spender, + newAllowance + ) + ); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20Upgradeable token, bytes memory data) + private + { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall( + data, + "SafeERC20: low-level call failed" + ); + if (returndata.length > 0) { + // Return data is optional + // solhint-disable-next-line max-line-length + require( + abi.decode(returndata, (bool)), + "SafeERC20: ERC20 operation did not succeed" + ); + } + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/TokenTimelockUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/TokenTimelockUpgradeable.sol new file mode 100644 index 0000000..774ff28 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC20/TokenTimelockUpgradeable.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./SafeERC20Upgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev A token holder contract that will allow a beneficiary to extract the + * tokens after a given release time. + * + * Useful for simple vesting schedules like "advisors get all of their tokens + * after 1 year". + */ +contract TokenTimelockUpgradeable is Initializable { + using SafeERC20Upgradeable for IERC20Upgradeable; + + // ERC20 basic token contract being held + IERC20Upgradeable private _token; + + // beneficiary of tokens after they are released + address private _beneficiary; + + // timestamp when token release is enabled + uint256 private _releaseTime; + + function __TokenTimelock_init( + IERC20Upgradeable token, + address beneficiary, + uint256 releaseTime + ) internal initializer { + __TokenTimelock_init_unchained(token, beneficiary, releaseTime); + } + + function __TokenTimelock_init_unchained( + IERC20Upgradeable token, + address beneficiary, + uint256 releaseTime + ) internal initializer { + // solhint-disable-next-line not-rely-on-time + require( + releaseTime > block.timestamp, + "TokenTimelock: release time is before current time" + ); + _token = token; + _beneficiary = beneficiary; + _releaseTime = releaseTime; + } + + /** + * @return the token being held. + */ + function token() public view returns (IERC20Upgradeable) { + return _token; + } + + /** + * @return the beneficiary of the tokens. + */ + function beneficiary() public view returns (address) { + return _beneficiary; + } + + /** + * @return the time when the tokens are released. + */ + function releaseTime() public view returns (uint256) { + return _releaseTime; + } + + /** + * @notice Transfers tokens held by timelock to beneficiary. + */ + function release() public virtual { + // solhint-disable-next-line not-rely-on-time + require( + block.timestamp >= _releaseTime, + "TokenTimelock: current time is before release time" + ); + + uint256 amount = _token.balanceOf(address(this)); + require(amount > 0, "TokenTimelock: no tokens to release"); + + _token.safeTransfer(_beneficiary, amount); + } + + uint256[47] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721BurnableUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721BurnableUpgradeable.sol new file mode 100644 index 0000000..9ca1600 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721BurnableUpgradeable.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../../GSN/ContextUpgradeable.sol"; +import "./ERC721Upgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @title ERC721 Burnable Token + * @dev ERC721 Token that can be irreversibly burned (destroyed). + */ +abstract contract ERC721BurnableUpgradeable is + Initializable, + ContextUpgradeable, + ERC721Upgradeable +{ + function __ERC721Burnable_init() internal initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __ERC721Burnable_init_unchained(); + } + + function __ERC721Burnable_init_unchained() internal initializer {} + + /** + * @dev Burns `tokenId`. See {ERC721-_burn}. + * + * Requirements: + * + * - The caller must own `tokenId` or be an approved operator. + */ + function burn(uint256 tokenId) public virtual { + //solhint-disable-next-line max-line-length + require( + _isApprovedOrOwner(_msgSender(), tokenId), + "ERC721Burnable: caller is not owner nor approved" + ); + _burn(tokenId); + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol new file mode 100644 index 0000000..5273416 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./IERC721ReceiverUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev Implementation of the {IERC721Receiver} interface. + * + * Accepts all token transfers. + * Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}. + */ +contract ERC721HolderUpgradeable is Initializable, IERC721ReceiverUpgradeable { + function __ERC721Holder_init() internal initializer { + __ERC721Holder_init_unchained(); + } + + function __ERC721Holder_init_unchained() internal initializer {} + + /** + * @dev See {IERC721Receiver-onERC721Received}. + * + * Always returns `IERC721Receiver.onERC721Received.selector`. + */ + function onERC721Received( + address, + address, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC721Received.selector; + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721PausableUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721PausableUpgradeable.sol new file mode 100644 index 0000000..3710ff0 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721PausableUpgradeable.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./ERC721Upgradeable.sol"; +import "../../utils/PausableUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev ERC721 token with pausable token transfers, minting and burning. + * + * Useful for scenarios such as preventing trades until the end of an evaluation + * period, or having an emergency switch for freezing all token transfers in the + * event of a large bug. + */ +abstract contract ERC721PausableUpgradeable is + Initializable, + ERC721Upgradeable, + PausableUpgradeable +{ + function __ERC721Pausable_init() internal initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __Pausable_init_unchained(); + __ERC721Pausable_init_unchained(); + } + + function __ERC721Pausable_init_unchained() internal initializer {} + + /** + * @dev See {ERC721-_beforeTokenTransfer}. + * + * Requirements: + * + * - the contract must not be paused. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override { + super._beforeTokenTransfer(from, to, tokenId); + + require(!paused(), "ERC721Pausable: token transfer while paused"); + } + + uint256[50] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol new file mode 100644 index 0000000..0a6d8e9 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol @@ -0,0 +1,604 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../../GSN/ContextUpgradeable.sol"; +import "./IERC721Upgradeable.sol"; +import "./IERC721MetadataUpgradeable.sol"; +import "./IERC721EnumerableUpgradeable.sol"; +import "./IERC721ReceiverUpgradeable.sol"; +import "../../introspection/ERC165Upgradeable.sol"; +import "../../math/SafeMathUpgradeable.sol"; +import "../../utils/AddressUpgradeable.sol"; +import "../../utils/EnumerableSetUpgradeable.sol"; +import "../../utils/EnumerableMapUpgradeable.sol"; +import "../../utils/StringsUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @title ERC721 Non-Fungible Token Standard basic implementation + * @dev see https://eips.ethereum.org/EIPS/eip-721 + */ +contract ERC721Upgradeable is + Initializable, + ContextUpgradeable, + ERC165Upgradeable, + IERC721Upgradeable, + IERC721MetadataUpgradeable, + IERC721EnumerableUpgradeable +{ + using SafeMathUpgradeable for uint256; + using AddressUpgradeable for address; + using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet; + using EnumerableMapUpgradeable for EnumerableMapUpgradeable.UintToAddressMap; + using StringsUpgradeable for uint256; + + // Equals to `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + // which can be also obtained as `IERC721Receiver(0).onERC721Received.selector` + bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; + + // Mapping from holder address to their (enumerable) set of owned tokens + mapping(address => EnumerableSetUpgradeable.UintSet) private _holderTokens; + + // Enumerable mapping from token ids to their owners + EnumerableMapUpgradeable.UintToAddressMap private _tokenOwners; + + // Mapping from token ID to approved address + mapping(uint256 => address) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // Optional mapping for token URIs + mapping(uint256 => string) private _tokenURIs; + + // Base URI + string private _baseURI; + + /* + * bytes4(keccak256('balanceOf(address)')) == 0x70a08231 + * bytes4(keccak256('ownerOf(uint256)')) == 0x6352211e + * bytes4(keccak256('approve(address,uint256)')) == 0x095ea7b3 + * bytes4(keccak256('getApproved(uint256)')) == 0x081812fc + * bytes4(keccak256('setApprovalForAll(address,bool)')) == 0xa22cb465 + * bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c5 + * bytes4(keccak256('transferFrom(address,address,uint256)')) == 0x23b872dd + * bytes4(keccak256('safeTransferFrom(address,address,uint256)')) == 0x42842e0e + * bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) == 0xb88d4fde + * + * => 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^ + * 0xa22cb465 ^ 0xe985e9c5 ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde == 0x80ac58cd + */ + bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd; + + /* + * bytes4(keccak256('name()')) == 0x06fdde03 + * bytes4(keccak256('symbol()')) == 0x95d89b41 + * bytes4(keccak256('tokenURI(uint256)')) == 0xc87b56dd + * + * => 0x06fdde03 ^ 0x95d89b41 ^ 0xc87b56dd == 0x5b5e139f + */ + bytes4 private constant _INTERFACE_ID_ERC721_METADATA = 0x5b5e139f; + + /* + * bytes4(keccak256('totalSupply()')) == 0x18160ddd + * bytes4(keccak256('tokenOfOwnerByIndex(address,uint256)')) == 0x2f745c59 + * bytes4(keccak256('tokenByIndex(uint256)')) == 0x4f6ccce7 + * + * => 0x18160ddd ^ 0x2f745c59 ^ 0x4f6ccce7 == 0x780e9d63 + */ + bytes4 private constant _INTERFACE_ID_ERC721_ENUMERABLE = 0x780e9d63; + + /** + * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. + */ + function __ERC721_init(string memory name, string memory symbol) + internal + initializer + { + __Context_init_unchained(); + __ERC165_init_unchained(); + __ERC721_init_unchained(name, symbol); + } + + function __ERC721_init_unchained(string memory name, string memory symbol) + internal + initializer + { + _name = name; + _symbol = symbol; + + // register the supported interfaces to conform to ERC721 via ERC165 + _registerInterface(_INTERFACE_ID_ERC721); + _registerInterface(_INTERFACE_ID_ERC721_METADATA); + _registerInterface(_INTERFACE_ID_ERC721_ENUMERABLE); + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public override view returns (uint256) { + require( + owner != address(0), + "ERC721: balance query for the zero address" + ); + + return _holderTokens[owner].length(); + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public override view returns (address) { + return + _tokenOwners.get( + tokenId, + "ERC721: owner query for nonexistent token" + ); + } + + /** + * @dev See {IERC721Metadata-name}. + */ + function name() public override view returns (string memory) { + return _name; + } + + /** + * @dev See {IERC721Metadata-symbol}. + */ + function symbol() public override view returns (string memory) { + return _symbol; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) + public + override + view + returns (string memory) + { + require( + _exists(tokenId), + "ERC721Metadata: URI query for nonexistent token" + ); + + string memory _tokenURI = _tokenURIs[tokenId]; + + // If there is no base URI, return the token URI. + if (bytes(_baseURI).length == 0) { + return _tokenURI; + } + // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). + if (bytes(_tokenURI).length > 0) { + return string(abi.encodePacked(_baseURI, _tokenURI)); + } + // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI. + return string(abi.encodePacked(_baseURI, tokenId.toString())); + } + + /** + * @dev Returns the base URI set via {_setBaseURI}. This will be + * automatically added as a prefix in {tokenURI} to each token's URI, or + * to the token ID if no specific URI is set for that token ID. + */ + function baseURI() public view returns (string memory) { + return _baseURI; + } + + /** + * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}. + */ + function tokenOfOwnerByIndex(address owner, uint256 index) + public + override + view + returns (uint256) + { + return _holderTokens[owner].at(index); + } + + /** + * @dev See {IERC721Enumerable-totalSupply}. + */ + function totalSupply() public override view returns (uint256) { + // _tokenOwners are indexed by tokenIds, so .length() returns the number of tokenIds + return _tokenOwners.length(); + } + + /** + * @dev See {IERC721Enumerable-tokenByIndex}. + */ + function tokenByIndex(uint256 index) + public + override + view + returns (uint256) + { + (uint256 tokenId, ) = _tokenOwners.at(index); + return tokenId; + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public virtual override { + address owner = ownerOf(tokenId); + require(to != owner, "ERC721: approval to current owner"); + + require( + _msgSender() == owner || isApprovedForAll(owner, _msgSender()), + "ERC721: approve caller is not owner nor approved for all" + ); + + _approve(to, tokenId); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) + public + override + view + returns (address) + { + require( + _exists(tokenId), + "ERC721: approved query for nonexistent token" + ); + + return _tokenApprovals[tokenId]; + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) + public + virtual + override + { + require(operator != _msgSender(), "ERC721: approve to caller"); + + _operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721-isApprovedForAll}. + */ + function isApprovedForAll(address owner, address operator) + public + override + view + returns (bool) + { + return _operatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override { + //solhint-disable-next-line max-line-length + require( + _isApprovedOrOwner(_msgSender(), tokenId), + "ERC721: transfer caller is not owner nor approved" + ); + + _transfer(from, to, tokenId); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) public virtual override { + require( + _isApprovedOrOwner(_msgSender(), tokenId), + "ERC721: transfer caller is not owner nor approved" + ); + _safeTransfer(from, to, tokenId, _data); + } + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * `_data` is additional data, it has no specified format and it is sent in call to `to`. + * + * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. + * implement alternative mechanisms to perform token transfer, such as signature-based. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeTransfer( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) internal virtual { + _transfer(from, to, tokenId); + require( + _checkOnERC721Received(from, to, tokenId, _data), + "ERC721: transfer to non ERC721Receiver implementer" + ); + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted (`_mint`), + * and stop existing when they are burned (`_burn`). + */ + function _exists(uint256 tokenId) internal view returns (bool) { + return _tokenOwners.contains(tokenId); + } + + /** + * @dev Returns whether `spender` is allowed to manage `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _isApprovedOrOwner(address spender, uint256 tokenId) + internal + view + returns (bool) + { + require( + _exists(tokenId), + "ERC721: operator query for nonexistent token" + ); + address owner = ownerOf(tokenId); + return (spender == owner || + getApproved(tokenId) == spender || + isApprovedForAll(owner, spender)); + } + + /** + * @dev Safely mints `tokenId` and transfers it to `to`. + * + * Requirements: + d* + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeMint(address to, uint256 tokenId) internal virtual { + _safeMint(to, tokenId, ""); + } + + /** + * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is + * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. + */ + function _safeMint( + address to, + uint256 tokenId, + bytes memory _data + ) internal virtual { + _mint(to, tokenId); + require( + _checkOnERC721Received(address(0), to, tokenId, _data), + "ERC721: transfer to non ERC721Receiver implementer" + ); + } + + /** + * @dev Mints `tokenId` and transfers it to `to`. + * + * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * Emits a {Transfer} event. + */ + function _mint(address to, uint256 tokenId) internal virtual { + require(to != address(0), "ERC721: mint to the zero address"); + require(!_exists(tokenId), "ERC721: token already minted"); + + _beforeTokenTransfer(address(0), to, tokenId); + + _holderTokens[to].add(tokenId); + + _tokenOwners.set(tokenId, to); + + emit Transfer(address(0), to, tokenId); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId) internal virtual { + address owner = ownerOf(tokenId); + + _beforeTokenTransfer(owner, address(0), tokenId); + + // Clear approvals + _approve(address(0), tokenId); + + // Clear metadata (if any) + if (bytes(_tokenURIs[tokenId]).length != 0) { + delete _tokenURIs[tokenId]; + } + + _holderTokens[owner].remove(tokenId); + + _tokenOwners.remove(tokenId); + + emit Transfer(owner, address(0), tokenId); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + function _transfer( + address from, + address to, + uint256 tokenId + ) internal virtual { + require( + ownerOf(tokenId) == from, + "ERC721: transfer of token that is not own" + ); + require(to != address(0), "ERC721: transfer to the zero address"); + + _beforeTokenTransfer(from, to, tokenId); + + // Clear approvals from the previous owner + _approve(address(0), tokenId); + + _holderTokens[from].remove(tokenId); + _holderTokens[to].add(tokenId); + + _tokenOwners.set(tokenId, to); + + emit Transfer(from, to, tokenId); + } + + /** + * @dev Sets `_tokenURI` as the tokenURI of `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _setTokenURI(uint256 tokenId, string memory _tokenURI) + internal + virtual + { + require( + _exists(tokenId), + "ERC721Metadata: URI set of nonexistent token" + ); + _tokenURIs[tokenId] = _tokenURI; + } + + /** + * @dev Internal function to set the base URI for all token IDs. It is + * automatically added as a prefix to the value returned in {tokenURI}, + * or to the token ID if {tokenURI} is empty. + */ + function _setBaseURI(string memory baseURI_) internal virtual { + _baseURI = baseURI_; + } + + /** + * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address. + * The call is not executed if the target address is not a contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return bool whether the call correctly returned the expected magic value + */ + function _checkOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) private returns (bool) { + if (!to.isContract()) { + return true; + } + bytes memory returndata = to.functionCall( + abi.encodeWithSelector( + IERC721ReceiverUpgradeable(to).onERC721Received.selector, + _msgSender(), + from, + tokenId, + _data + ), + "ERC721: transfer to non ERC721Receiver implementer" + ); + bytes4 retval = abi.decode(returndata, (bytes4)); + return (retval == _ERC721_RECEIVED); + } + + function _approve(address to, uint256 tokenId) private { + _tokenApprovals[tokenId] = to; + emit Approval(ownerOf(tokenId), to, tokenId); + } + + /** + * @dev Hook that is called before any token transfer. This includes minting + * and burning. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, ``from``'s `tokenId` will be burned. + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} + + uint256[41] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721EnumerableUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721EnumerableUpgradeable.sol new file mode 100644 index 0000000..d9cfaa5 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721EnumerableUpgradeable.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.2; + +import "./IERC721Upgradeable.sol"; + +/** + * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension + * @dev See https://eips.ethereum.org/EIPS/eip-721 + */ +interface IERC721EnumerableUpgradeable is IERC721Upgradeable { + /** + * @dev Returns the total amount of tokens stored by the contract. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns a token ID owned by `owner` at a given `index` of its token list. + * Use along with {balanceOf} to enumerate all of ``owner``'s tokens. + */ + function tokenOfOwnerByIndex(address owner, uint256 index) + external + view + returns (uint256 tokenId); + + /** + * @dev Returns a token ID at a given `index` of all the tokens stored by the contract. + * Use along with {totalSupply} to enumerate all tokens. + */ + function tokenByIndex(uint256 index) external view returns (uint256); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721MetadataUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721MetadataUpgradeable.sol new file mode 100644 index 0000000..377d56b --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721MetadataUpgradeable.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.2; + +import "./IERC721Upgradeable.sol"; + +/** + * @title ERC-721 Non-Fungible Token Standard, optional metadata extension + * @dev See https://eips.ethereum.org/EIPS/eip-721 + */ +interface IERC721MetadataUpgradeable is IERC721Upgradeable { + /** + * @dev Returns the token collection name. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the token collection symbol. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) external view returns (string memory); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol new file mode 100644 index 0000000..6a6e3f6 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @title ERC721 token receiver interface + * @dev Interface for any contract that wants to support safeTransfers + * from ERC721 asset contracts. + */ +interface IERC721ReceiverUpgradeable { + /** + * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} + * by `operator` from `from`, this function is called. + * + * It must return its Solidity selector to confirm the token transfer. + * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + * + * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`. + */ + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol new file mode 100644 index 0000000..d5c175d --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.2; + +import "../../introspection/IERC165Upgradeable.sol"; + +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721Upgradeable is IERC165Upgradeable { + /** + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + */ + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + + /** + * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. + */ + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. + */ + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); + + /** + * @dev Returns the number of tokens in ``owner``'s account. + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /** + * @dev Transfers `tokenId` token from `from` to `to`. + * + * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function approve(address to, uint256 tokenId) external; + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) + external + view + returns (address operator); + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool _approved) external; + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll} + */ + function isApprovedForAll(address owner, address operator) + external + view + returns (bool); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/README.adoc new file mode 100644 index 0000000..5f584ee --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC721/README.adoc @@ -0,0 +1,42 @@ += ERC 721 + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc721 + +This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-721[ERC721 Non-Fungible Token Standard]. + +TIP: For a walk through on how to create an ERC721 token read our xref:ROOT:erc721.adoc[ERC721 guide]. + +The EIP consists of three interfaces, found here as {IERC721}, {IERC721Metadata}, and {IERC721Enumerable}. Only the first one is required in a contract to be ERC721 compliant. However, all three are implemented in {ERC721}. + +Additionally, {IERC721Receiver} can be used to prevent tokens from becoming forever locked in contracts. Imagine sending an in-game item to an exchange address that can't send it back!. When using <>, the token contract checks to see that the receiver is an {IERC721Receiver}, which implies that it knows how to handle {ERC721} tokens. If you're writing a contract that needs to receive {ERC721} tokens, you'll want to include this interface. + +Additionally there are multiple custom extensions, including: + +* designation of addresses that can pause token transfers for all users ({ERC721Pausable}). +* destruction of own tokens ({ERC721Burnable}). + +NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC721 (such as <>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc721.adoc#Presets[ERC721 Presets] (such as {ERC721PresetMinterPauserAutoId}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts. + + +== Core + +{{IERC721}} + +{{IERC721Metadata}} + +{{IERC721Enumerable}} + +{{ERC721}} + +{{IERC721Receiver}} + +== Extensions + +{{ERC721Pausable}} + +{{ERC721Burnable}} + +== Convenience + +{{ERC721Holder}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/ERC777Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/ERC777Upgradeable.sol new file mode 100644 index 0000000..d52c413 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/ERC777Upgradeable.sol @@ -0,0 +1,646 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../../GSN/ContextUpgradeable.sol"; +import "./IERC777Upgradeable.sol"; +import "./IERC777RecipientUpgradeable.sol"; +import "./IERC777SenderUpgradeable.sol"; +import "../../token/ERC20/IERC20Upgradeable.sol"; +import "../../math/SafeMathUpgradeable.sol"; +import "../../utils/AddressUpgradeable.sol"; +import "../../introspection/IERC1820RegistryUpgradeable.sol"; +import "../../proxy/Initializable.sol"; + +/** + * @dev Implementation of the {IERC777} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * + * Support for ERC20 is included in this contract, as specified by the EIP: both + * the ERC777 and ERC20 interfaces can be safely used when interacting with it. + * Both {IERC777-Sent} and {IERC20-Transfer} events are emitted on token + * movements. + * + * Additionally, the {IERC777-granularity} value is hard-coded to `1`, meaning that there + * are no special restrictions in the amount of tokens that created, moved, or + * destroyed. This makes integration with ERC20 applications seamless. + */ +contract ERC777Upgradeable is + Initializable, + ContextUpgradeable, + IERC777Upgradeable, + IERC20Upgradeable +{ + using SafeMathUpgradeable for uint256; + using AddressUpgradeable for address; + + IERC1820RegistryUpgradeable + internal constant _ERC1820_REGISTRY = IERC1820RegistryUpgradeable( + 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 + ); + + mapping(address => uint256) private _balances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + // We inline the result of the following hashes because Solidity doesn't resolve them at compile time. + // See https://github.com/ethereum/solidity/issues/4024. + + // keccak256("ERC777TokensSender") + bytes32 + private constant _TOKENS_SENDER_INTERFACE_HASH = 0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895; + + // keccak256("ERC777TokensRecipient") + bytes32 + private constant _TOKENS_RECIPIENT_INTERFACE_HASH = 0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b; + + // This isn't ever read from - it's only used to respond to the defaultOperators query. + address[] private _defaultOperatorsArray; + + // Immutable, but accounts may revoke them (tracked in __revokedDefaultOperators). + mapping(address => bool) private _defaultOperators; + + // For each account, a mapping of its operators and revoked default operators. + mapping(address => mapping(address => bool)) private _operators; + mapping(address => mapping(address => bool)) + private _revokedDefaultOperators; + + // ERC20-allowances + mapping(address => mapping(address => uint256)) private _allowances; + + /** + * @dev `defaultOperators` may be an empty array. + */ + function __ERC777_init( + string memory name, + string memory symbol, + address[] memory defaultOperators + ) internal initializer { + __Context_init_unchained(); + __ERC777_init_unchained(name, symbol, defaultOperators); + } + + function __ERC777_init_unchained( + string memory name, + string memory symbol, + address[] memory defaultOperators + ) internal initializer { + _name = name; + _symbol = symbol; + + _defaultOperatorsArray = defaultOperators; + for (uint256 i = 0; i < _defaultOperatorsArray.length; i++) { + _defaultOperators[_defaultOperatorsArray[i]] = true; + } + + // register interfaces + _ERC1820_REGISTRY.setInterfaceImplementer( + address(this), + keccak256("ERC777Token"), + address(this) + ); + _ERC1820_REGISTRY.setInterfaceImplementer( + address(this), + keccak256("ERC20Token"), + address(this) + ); + } + + /** + * @dev See {IERC777-name}. + */ + function name() public override view returns (string memory) { + return _name; + } + + /** + * @dev See {IERC777-symbol}. + */ + function symbol() public override view returns (string memory) { + return _symbol; + } + + /** + * @dev See {ERC20-decimals}. + * + * Always returns 18, as per the + * [ERC777 EIP](https://eips.ethereum.org/EIPS/eip-777#backward-compatibility). + */ + function decimals() public pure returns (uint8) { + return 18; + } + + /** + * @dev See {IERC777-granularity}. + * + * This implementation always returns `1`. + */ + function granularity() public override view returns (uint256) { + return 1; + } + + /** + * @dev See {IERC777-totalSupply}. + */ + function totalSupply() + public + override(IERC20Upgradeable, IERC777Upgradeable) + view + returns (uint256) + { + return _totalSupply; + } + + /** + * @dev Returns the amount of tokens owned by an account (`tokenHolder`). + */ + function balanceOf(address tokenHolder) + public + override(IERC20Upgradeable, IERC777Upgradeable) + view + returns (uint256) + { + return _balances[tokenHolder]; + } + + /** + * @dev See {IERC777-send}. + * + * Also emits a {IERC20-Transfer} event for ERC20 compatibility. + */ + function send( + address recipient, + uint256 amount, + bytes memory data + ) public override { + _send(_msgSender(), recipient, amount, data, "", true); + } + + /** + * @dev See {IERC20-transfer}. + * + * Unlike `send`, `recipient` is _not_ required to implement the {IERC777Recipient} + * interface if it is a contract. + * + * Also emits a {Sent} event. + */ + function transfer(address recipient, uint256 amount) + public + override + returns (bool) + { + require( + recipient != address(0), + "ERC777: transfer to the zero address" + ); + + address from = _msgSender(); + + _callTokensToSend(from, from, recipient, amount, "", ""); + + _move(from, from, recipient, amount, "", ""); + + _callTokensReceived(from, from, recipient, amount, "", "", false); + + return true; + } + + /** + * @dev See {IERC777-burn}. + * + * Also emits a {IERC20-Transfer} event for ERC20 compatibility. + */ + function burn(uint256 amount, bytes memory data) public override { + _burn(_msgSender(), amount, data, ""); + } + + /** + * @dev See {IERC777-isOperatorFor}. + */ + function isOperatorFor(address operator, address tokenHolder) + public + override + view + returns (bool) + { + return + operator == tokenHolder || + (_defaultOperators[operator] && + !_revokedDefaultOperators[tokenHolder][operator]) || + _operators[tokenHolder][operator]; + } + + /** + * @dev See {IERC777-authorizeOperator}. + */ + function authorizeOperator(address operator) public override { + require( + _msgSender() != operator, + "ERC777: authorizing self as operator" + ); + + if (_defaultOperators[operator]) { + delete _revokedDefaultOperators[_msgSender()][operator]; + } else { + _operators[_msgSender()][operator] = true; + } + + emit AuthorizedOperator(operator, _msgSender()); + } + + /** + * @dev See {IERC777-revokeOperator}. + */ + function revokeOperator(address operator) public override { + require(operator != _msgSender(), "ERC777: revoking self as operator"); + + if (_defaultOperators[operator]) { + _revokedDefaultOperators[_msgSender()][operator] = true; + } else { + delete _operators[_msgSender()][operator]; + } + + emit RevokedOperator(operator, _msgSender()); + } + + /** + * @dev See {IERC777-defaultOperators}. + */ + function defaultOperators() + public + override + view + returns (address[] memory) + { + return _defaultOperatorsArray; + } + + /** + * @dev See {IERC777-operatorSend}. + * + * Emits {Sent} and {IERC20-Transfer} events. + */ + function operatorSend( + address sender, + address recipient, + uint256 amount, + bytes memory data, + bytes memory operatorData + ) public override { + require( + isOperatorFor(_msgSender(), sender), + "ERC777: caller is not an operator for holder" + ); + _send(sender, recipient, amount, data, operatorData, true); + } + + /** + * @dev See {IERC777-operatorBurn}. + * + * Emits {Burned} and {IERC20-Transfer} events. + */ + function operatorBurn( + address account, + uint256 amount, + bytes memory data, + bytes memory operatorData + ) public override { + require( + isOperatorFor(_msgSender(), account), + "ERC777: caller is not an operator for holder" + ); + _burn(account, amount, data, operatorData); + } + + /** + * @dev See {IERC20-allowance}. + * + * Note that operator and allowance concepts are orthogonal: operators may + * not have allowance, and accounts with allowance may not be operators + * themselves. + */ + function allowance(address holder, address spender) + public + override + view + returns (uint256) + { + return _allowances[holder][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Note that accounts cannot have allowance issued by their operators. + */ + function approve(address spender, uint256 value) + public + override + returns (bool) + { + address holder = _msgSender(); + _approve(holder, spender, value); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Note that operator and allowance concepts are orthogonal: operators cannot + * call `transferFrom` (unless they have allowance), and accounts with + * allowance cannot call `operatorSend` (unless they are operators). + * + * Emits {Sent}, {IERC20-Transfer} and {IERC20-Approval} events. + */ + function transferFrom( + address holder, + address recipient, + uint256 amount + ) public override returns (bool) { + require( + recipient != address(0), + "ERC777: transfer to the zero address" + ); + require(holder != address(0), "ERC777: transfer from the zero address"); + + address spender = _msgSender(); + + _callTokensToSend(spender, holder, recipient, amount, "", ""); + + _move(spender, holder, recipient, amount, "", ""); + _approve( + holder, + spender, + _allowances[holder][spender].sub( + amount, + "ERC777: transfer amount exceeds allowance" + ) + ); + + _callTokensReceived(spender, holder, recipient, amount, "", "", false); + + return true; + } + + /** + * @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * If a send hook is registered for `account`, the corresponding function + * will be called with `operator`, `data` and `operatorData`. + * + * See {IERC777Sender} and {IERC777Recipient}. + * + * Emits {Minted} and {IERC20-Transfer} events. + * + * Requirements + * + * - `account` cannot be the zero address. + * - if `account` is a contract, it must implement the {IERC777Recipient} + * interface. + */ + function _mint( + address account, + uint256 amount, + bytes memory userData, + bytes memory operatorData + ) internal virtual { + require(account != address(0), "ERC777: mint to the zero address"); + + address operator = _msgSender(); + + _beforeTokenTransfer(operator, address(0), account, amount); + + // Update state variables + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + + _callTokensReceived( + operator, + address(0), + account, + amount, + userData, + operatorData, + true + ); + + emit Minted(operator, account, amount, userData, operatorData); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Send tokens + * @param from address token holder address + * @param to address recipient address + * @param amount uint256 amount of tokens to transfer + * @param userData bytes extra information provided by the token holder (if any) + * @param operatorData bytes extra information provided by the operator (if any) + * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient + */ + function _send( + address from, + address to, + uint256 amount, + bytes memory userData, + bytes memory operatorData, + bool requireReceptionAck + ) internal { + require(from != address(0), "ERC777: send from the zero address"); + require(to != address(0), "ERC777: send to the zero address"); + + address operator = _msgSender(); + + _callTokensToSend(operator, from, to, amount, userData, operatorData); + + _move(operator, from, to, amount, userData, operatorData); + + _callTokensReceived( + operator, + from, + to, + amount, + userData, + operatorData, + requireReceptionAck + ); + } + + /** + * @dev Burn tokens + * @param from address token holder address + * @param amount uint256 amount of tokens to burn + * @param data bytes extra information provided by the token holder + * @param operatorData bytes extra information provided by the operator (if any) + */ + function _burn( + address from, + uint256 amount, + bytes memory data, + bytes memory operatorData + ) internal virtual { + require(from != address(0), "ERC777: burn from the zero address"); + + address operator = _msgSender(); + + _beforeTokenTransfer(operator, from, address(0), amount); + + _callTokensToSend( + operator, + from, + address(0), + amount, + data, + operatorData + ); + + // Update state variables + _balances[from] = _balances[from].sub( + amount, + "ERC777: burn amount exceeds balance" + ); + _totalSupply = _totalSupply.sub(amount); + + emit Burned(operator, from, amount, data, operatorData); + emit Transfer(from, address(0), amount); + } + + function _move( + address operator, + address from, + address to, + uint256 amount, + bytes memory userData, + bytes memory operatorData + ) private { + _beforeTokenTransfer(operator, from, to, amount); + + _balances[from] = _balances[from].sub( + amount, + "ERC777: transfer amount exceeds balance" + ); + _balances[to] = _balances[to].add(amount); + + emit Sent(operator, from, to, amount, userData, operatorData); + emit Transfer(from, to, amount); + } + + /** + * @dev See {ERC20-_approve}. + * + * Note that accounts cannot have allowance issued by their operators. + */ + function _approve( + address holder, + address spender, + uint256 value + ) internal { + require(holder != address(0), "ERC777: approve from the zero address"); + require(spender != address(0), "ERC777: approve to the zero address"); + + _allowances[holder][spender] = value; + emit Approval(holder, spender, value); + } + + /** + * @dev Call from.tokensToSend() if the interface is registered + * @param operator address operator requesting the transfer + * @param from address token holder address + * @param to address recipient address + * @param amount uint256 amount of tokens to transfer + * @param userData bytes extra information provided by the token holder (if any) + * @param operatorData bytes extra information provided by the operator (if any) + */ + function _callTokensToSend( + address operator, + address from, + address to, + uint256 amount, + bytes memory userData, + bytes memory operatorData + ) private { + address implementer = _ERC1820_REGISTRY.getInterfaceImplementer( + from, + _TOKENS_SENDER_INTERFACE_HASH + ); + if (implementer != address(0)) { + IERC777SenderUpgradeable(implementer).tokensToSend( + operator, + from, + to, + amount, + userData, + operatorData + ); + } + } + + /** + * @dev Call to.tokensReceived() if the interface is registered. Reverts if the recipient is a contract but + * tokensReceived() was not registered for the recipient + * @param operator address operator requesting the transfer + * @param from address token holder address + * @param to address recipient address + * @param amount uint256 amount of tokens to transfer + * @param userData bytes extra information provided by the token holder (if any) + * @param operatorData bytes extra information provided by the operator (if any) + * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient + */ + function _callTokensReceived( + address operator, + address from, + address to, + uint256 amount, + bytes memory userData, + bytes memory operatorData, + bool requireReceptionAck + ) private { + address implementer = _ERC1820_REGISTRY.getInterfaceImplementer( + to, + _TOKENS_RECIPIENT_INTERFACE_HASH + ); + if (implementer != address(0)) { + IERC777RecipientUpgradeable(implementer).tokensReceived( + operator, + from, + to, + amount, + userData, + operatorData + ); + } else if (requireReceptionAck) { + require( + !to.isContract(), + "ERC777: token recipient contract has no implementer for ERC777TokensRecipient" + ); + } + } + + /** + * @dev Hook that is called before any token transfer. This includes + * calls to {send}, {transfer}, {operatorSend}, minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be to transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256 amount + ) internal virtual {} + + uint256[41] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/IERC777RecipientUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/IERC777RecipientUpgradeable.sol new file mode 100644 index 0000000..20b5a1b --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/IERC777RecipientUpgradeable.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Interface of the ERC777TokensRecipient standard as defined in the EIP. + * + * Accounts can be notified of {IERC777} tokens being sent to them by having a + * contract implement this interface (contract holders can be their own + * implementer) and registering it on the + * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. + * + * See {IERC1820Registry} and {ERC1820Implementer}. + */ +interface IERC777RecipientUpgradeable { + /** + * @dev Called by an {IERC777} token contract whenever tokens are being + * moved or created into a registered account (`to`). The type of operation + * is conveyed by `from` being the zero address or not. + * + * This call occurs _after_ the token contract's state is updated, so + * {IERC777-balanceOf}, etc., can be used to query the post-operation state. + * + * This function may revert to prevent the operation from being executed. + */ + function tokensReceived( + address operator, + address from, + address to, + uint256 amount, + bytes calldata userData, + bytes calldata operatorData + ) external; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/IERC777SenderUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/IERC777SenderUpgradeable.sol new file mode 100644 index 0000000..56d0659 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/IERC777SenderUpgradeable.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Interface of the ERC777TokensSender standard as defined in the EIP. + * + * {IERC777} Token holders can be notified of operations performed on their + * tokens by having a contract implement this interface (contract holders can be + * their own implementer) and registering it on the + * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. + * + * See {IERC1820Registry} and {ERC1820Implementer}. + */ +interface IERC777SenderUpgradeable { + /** + * @dev Called by an {IERC777} token contract whenever a registered holder's + * (`from`) tokens are about to be moved or destroyed. The type of operation + * is conveyed by `to` being the zero address or not. + * + * This call occurs _before_ the token contract's state is updated, so + * {IERC777-balanceOf}, etc., can be used to query the pre-operation state. + * + * This function may revert to prevent the operation from being executed. + */ + function tokensToSend( + address operator, + address from, + address to, + uint256 amount, + bytes calldata userData, + bytes calldata operatorData + ) external; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/IERC777Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/IERC777Upgradeable.sol new file mode 100644 index 0000000..ccd758a --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/IERC777Upgradeable.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Interface of the ERC777Token standard as defined in the EIP. + * + * This contract uses the + * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 registry standard] to let + * token holders and recipients react to token movements by using setting implementers + * for the associated interfaces in said registry. See {IERC1820Registry} and + * {ERC1820Implementer}. + */ +interface IERC777Upgradeable { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the smallest part of the token that is not divisible. This + * means all token operations (creation, movement and destruction) must have + * amounts that are a multiple of this number. + * + * For most token contracts, this value will equal 1. + */ + function granularity() external view returns (uint256); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by an account (`owner`). + */ + function balanceOf(address owner) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * If send or receive hooks are registered for the caller and `recipient`, + * the corresponding functions will be called with `data` and empty + * `operatorData`. See {IERC777Sender} and {IERC777Recipient}. + * + * Emits a {Sent} event. + * + * Requirements + * + * - the caller must have at least `amount` tokens. + * - `recipient` cannot be the zero address. + * - if `recipient` is a contract, it must implement the {IERC777Recipient} + * interface. + */ + function send( + address recipient, + uint256 amount, + bytes calldata data + ) external; + + /** + * @dev Destroys `amount` tokens from the caller's account, reducing the + * total supply. + * + * If a send hook is registered for the caller, the corresponding function + * will be called with `data` and empty `operatorData`. See {IERC777Sender}. + * + * Emits a {Burned} event. + * + * Requirements + * + * - the caller must have at least `amount` tokens. + */ + function burn(uint256 amount, bytes calldata data) external; + + /** + * @dev Returns true if an account is an operator of `tokenHolder`. + * Operators can send and burn tokens on behalf of their owners. All + * accounts are their own operator. + * + * See {operatorSend} and {operatorBurn}. + */ + function isOperatorFor(address operator, address tokenHolder) + external + view + returns (bool); + + /** + * @dev Make an account an operator of the caller. + * + * See {isOperatorFor}. + * + * Emits an {AuthorizedOperator} event. + * + * Requirements + * + * - `operator` cannot be calling address. + */ + function authorizeOperator(address operator) external; + + /** + * @dev Revoke an account's operator status for the caller. + * + * See {isOperatorFor} and {defaultOperators}. + * + * Emits a {RevokedOperator} event. + * + * Requirements + * + * - `operator` cannot be calling address. + */ + function revokeOperator(address operator) external; + + /** + * @dev Returns the list of default operators. These accounts are operators + * for all token holders, even if {authorizeOperator} was never called on + * them. + * + * This list is immutable, but individual holders may revoke these via + * {revokeOperator}, in which case {isOperatorFor} will return false. + */ + function defaultOperators() external view returns (address[] memory); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient`. The caller must + * be an operator of `sender`. + * + * If send or receive hooks are registered for `sender` and `recipient`, + * the corresponding functions will be called with `data` and + * `operatorData`. See {IERC777Sender} and {IERC777Recipient}. + * + * Emits a {Sent} event. + * + * Requirements + * + * - `sender` cannot be the zero address. + * - `sender` must have at least `amount` tokens. + * - the caller must be an operator for `sender`. + * - `recipient` cannot be the zero address. + * - if `recipient` is a contract, it must implement the {IERC777Recipient} + * interface. + */ + function operatorSend( + address sender, + address recipient, + uint256 amount, + bytes calldata data, + bytes calldata operatorData + ) external; + + /** + * @dev Destroys `amount` tokens from `account`, reducing the total supply. + * The caller must be an operator of `account`. + * + * If a send hook is registered for `account`, the corresponding function + * will be called with `data` and `operatorData`. See {IERC777Sender}. + * + * Emits a {Burned} event. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + * - the caller must be an operator for `account`. + */ + function operatorBurn( + address account, + uint256 amount, + bytes calldata data, + bytes calldata operatorData + ) external; + + event Sent( + address indexed operator, + address indexed from, + address indexed to, + uint256 amount, + bytes data, + bytes operatorData + ); + + event Minted( + address indexed operator, + address indexed to, + uint256 amount, + bytes data, + bytes operatorData + ); + + event Burned( + address indexed operator, + address indexed from, + uint256 amount, + bytes data, + bytes operatorData + ); + + event AuthorizedOperator( + address indexed operator, + address indexed tokenHolder + ); + + event RevokedOperator( + address indexed operator, + address indexed tokenHolder + ); +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/README.adoc new file mode 100644 index 0000000..4dee8a6 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/token/ERC777/README.adoc @@ -0,0 +1,24 @@ += ERC 777 + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc777 + +This set of interfaces and contracts are all related to the [ERC777 token standard](https://eips.ethereum.org/EIPS/eip-777). + +TIP: For an overview of ERC777 tokens and a walk through on how to create a token contract read our xref:ROOT:erc777.adoc[ERC777 guide]. + +The token behavior itself is implemented in the core contracts: {IERC777}, {ERC777}. + +Additionally there are interfaces used to develop contracts that react to token movements: {IERC777Sender}, {IERC777Recipient}. + +== Core + +{{IERC777}} + +{{ERC777}} + +== Hooks + +{{IERC777Sender}} + +{{IERC777Recipient}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol new file mode 100644 index 0000000..b5ea503 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.2; + +/** + * @dev Collection of functions related to the address type + */ +library AddressUpgradeable { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies in extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { + size := extcodesize(account) + } + return size > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require( + address(this).balance >= amount, + "Address: insufficient balance" + ); + + // solhint-disable-next-line avoid-low-level-calls, avoid-call-value + (bool success, ) = recipient.call{value: amount}(""); + require( + success, + "Address: unable to send value, recipient may have reverted" + ); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain`call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) + internal + returns (bytes memory) + { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + return _functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value + ) internal returns (bytes memory) { + return + functionCallWithValue( + target, + data, + value, + "Address: low-level call with value failed" + ); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { + require( + address(this).balance >= value, + "Address: insufficient balance for call" + ); + return _functionCallWithValue(target, data, value, errorMessage); + } + + function _functionCallWithValue( + address target, + bytes memory data, + uint256 weiValue, + string memory errorMessage + ) private returns (bytes memory) { + require(isContract(target), "Address: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.call{value: weiValue}( + data + ); + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/ArraysUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/ArraysUpgradeable.sol new file mode 100644 index 0000000..6c0d229 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/ArraysUpgradeable.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../math/MathUpgradeable.sol"; + +/** + * @dev Collection of functions related to array types. + */ +library ArraysUpgradeable { + /** + * @dev Searches a sorted `array` and returns the first index that contains + * a value greater or equal to `element`. If no such index exists (i.e. all + * values in the array are strictly less than `element`), the array length is + * returned. Time complexity O(log n). + * + * `array` is expected to be sorted in ascending order, and to contain no + * repeated elements. + */ + function findUpperBound(uint256[] storage array, uint256 element) + internal + view + returns (uint256) + { + if (array.length == 0) { + return 0; + } + + uint256 low = 0; + uint256 high = array.length; + + while (low < high) { + uint256 mid = MathUpgradeable.average(low, high); + + // Note that mid will always be strictly less than high (i.e. it will be a valid array index) + // because Math.average rounds down (it does integer division with truncation). + if (array[mid] > element) { + high = mid; + } else { + low = mid + 1; + } + } + + // At this point `low` is the exclusive upper bound. We will return the inclusive upper bound. + if (low > 0 && array[low - 1] == element) { + return low - 1; + } else { + return low; + } + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol new file mode 100644 index 0000000..bb27627 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../math/SafeMathUpgradeable.sol"; + +/** + * @title Counters + * @author Matt Condon (@shrugs) + * @dev Provides counters that can only be incremented or decremented by one. This can be used e.g. to track the number + * of elements in a mapping, issuing ERC721 ids, or counting request ids. + * + * Include with `using Counters for Counters.Counter;` + * Since it is not possible to overflow a 256 bit integer with increments of one, `increment` can skip the {SafeMath} + * overflow check, thereby saving gas. This does assume however correct usage, in that the underlying `_value` is never + * directly accessed. + */ +library CountersUpgradeable { + using SafeMathUpgradeable for uint256; + + struct Counter { + // This variable should never be directly accessed by users of the library: interactions must be restricted to + // the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add + // this feature: see https://github.com/ethereum/solidity/issues/4637 + uint256 _value; // default: 0 + } + + function current(Counter storage counter) internal view returns (uint256) { + return counter._value; + } + + function increment(Counter storage counter) internal { + // The {SafeMath} overflow check can be skipped here, see the comment at the top + counter._value += 1; + } + + function decrement(Counter storage counter) internal { + counter._value = counter._value.sub(1); + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/Create2Upgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/Create2Upgradeable.sol new file mode 100644 index 0000000..bbdcd3a --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/Create2Upgradeable.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer. + * `CREATE2` can be used to compute in advance the address where a smart + * contract will be deployed, which allows for interesting new mechanisms known + * as 'counterfactual interactions'. + * + * See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more + * information. + */ +library Create2Upgradeable { + /** + * @dev Deploys a contract using `CREATE2`. The address where the contract + * will be deployed can be known in advance via {computeAddress}. + * + * The bytecode for a contract can be obtained from Solidity with + * `type(contractName).creationCode`. + * + * Requirements: + * + * - `bytecode` must not be empty. + * - `salt` must have not been used for `bytecode` already. + * - the factory must have a balance of at least `amount`. + * - if `amount` is non-zero, `bytecode` must have a `payable` constructor. + */ + function deploy( + uint256 amount, + bytes32 salt, + bytes memory bytecode + ) internal returns (address) { + address addr; + require( + address(this).balance >= amount, + "Create2: insufficient balance" + ); + require(bytecode.length != 0, "Create2: bytecode length is zero"); + // solhint-disable-next-line no-inline-assembly + assembly { + addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) + } + require(addr != address(0), "Create2: Failed on deploy"); + return addr; + } + + /** + * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the + * `bytecodeHash` or `salt` will result in a new destination address. + */ + function computeAddress(bytes32 salt, bytes32 bytecodeHash) + internal + view + returns (address) + { + return computeAddress(salt, bytecodeHash, address(this)); + } + + /** + * @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at + * `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}. + */ + function computeAddress( + bytes32 salt, + bytes32 bytecodeHash, + address deployer + ) internal pure returns (address) { + bytes32 _data = keccak256( + abi.encodePacked(bytes1(0xff), deployer, salt, bytecodeHash) + ); + return address(uint256(_data)); + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/EnumerableMapUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/EnumerableMapUpgradeable.sol new file mode 100644 index 0000000..953dad2 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/EnumerableMapUpgradeable.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Library for managing an enumerable variant of Solidity's + * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] + * type. + * + * Maps have the following properties: + * + * - Entries are added, removed, and checked for existence in constant time + * (O(1)). + * - Entries are enumerated in O(n). No guarantees are made on the ordering. + * + * ``` + * contract Example { + * // Add the library methods + * using EnumerableMap for EnumerableMap.UintToAddressMap; + * + * // Declare a set state variable + * EnumerableMap.UintToAddressMap private myMap; + * } + * ``` + * + * As of v3.0.0, only maps of type `uint256 -> address` (`UintToAddressMap`) are + * supported. + */ +library EnumerableMapUpgradeable { + // To implement this library for multiple types with as little code + // repetition as possible, we write it in terms of a generic Map type with + // bytes32 keys and values. + // The Map implementation uses private functions, and user-facing + // implementations (such as Uint256ToAddressMap) are just wrappers around + // the underlying Map. + // This means that we can only create new EnumerableMaps for types that fit + // in bytes32. + + struct MapEntry { + bytes32 _key; + bytes32 _value; + } + + struct Map { + // Storage of map keys and values + MapEntry[] _entries; + // Position of the entry defined by a key in the `entries` array, plus 1 + // because index 0 means a key is not in the map. + mapping(bytes32 => uint256) _indexes; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function _set( + Map storage map, + bytes32 key, + bytes32 value + ) private returns (bool) { + // We read and store the key's index to prevent multiple reads from the same storage slot + uint256 keyIndex = map._indexes[key]; + + if (keyIndex == 0) { + // Equivalent to !contains(map, key) + map._entries.push(MapEntry({_key: key, _value: value})); + // The entry is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + map._indexes[key] = map._entries.length; + return true; + } else { + map._entries[keyIndex - 1]._value = value; + return false; + } + } + + /** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function _remove(Map storage map, bytes32 key) private returns (bool) { + // We read and store the key's index to prevent multiple reads from the same storage slot + uint256 keyIndex = map._indexes[key]; + + if (keyIndex != 0) { + // Equivalent to contains(map, key) + // To delete a key-value pair from the _entries array in O(1), we swap the entry to delete with the last one + // in the array, and then remove the last entry (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 toDeleteIndex = keyIndex - 1; + uint256 lastIndex = map._entries.length - 1; + + // When the entry to delete is the last one, the swap operation is unnecessary. However, since this occurs + // so rarely, we still do the swap anyway to avoid the gas cost of adding an 'if' statement. + + MapEntry storage lastEntry = map._entries[lastIndex]; + + // Move the last entry to the index where the entry to delete is + map._entries[toDeleteIndex] = lastEntry; + // Update the index for the moved entry + map._indexes[lastEntry._key] = toDeleteIndex + 1; // All indexes are 1-based + + // Delete the slot where the moved entry was stored + map._entries.pop(); + + // Delete the index for the deleted slot + delete map._indexes[key]; + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function _contains(Map storage map, bytes32 key) + private + view + returns (bool) + { + return map._indexes[key] != 0; + } + + /** + * @dev Returns the number of key-value pairs in the map. O(1). + */ + function _length(Map storage map) private view returns (uint256) { + return map._entries.length; + } + + /** + * @dev Returns the key-value pair stored at position `index` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function _at(Map storage map, uint256 index) + private + view + returns (bytes32, bytes32) + { + require( + map._entries.length > index, + "EnumerableMap: index out of bounds" + ); + + MapEntry storage entry = map._entries[index]; + return (entry._key, entry._value); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function _get(Map storage map, bytes32 key) private view returns (bytes32) { + return _get(map, key, "EnumerableMap: nonexistent key"); + } + + /** + * @dev Same as {_get}, with a custom error message when `key` is not in the map. + */ + function _get( + Map storage map, + bytes32 key, + string memory errorMessage + ) private view returns (bytes32) { + uint256 keyIndex = map._indexes[key]; + require(keyIndex != 0, errorMessage); // Equivalent to contains(map, key) + return map._entries[keyIndex - 1]._value; // All indexes are 1-based + } + + // UintToAddressMap + + struct UintToAddressMap { + Map _inner; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set( + UintToAddressMap storage map, + uint256 key, + address value + ) internal returns (bool) { + return _set(map._inner, bytes32(key), bytes32(uint256(value))); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(UintToAddressMap storage map, uint256 key) + internal + returns (bool) + { + return _remove(map._inner, bytes32(key)); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(UintToAddressMap storage map, uint256 key) + internal + view + returns (bool) + { + return _contains(map._inner, bytes32(key)); + } + + /** + * @dev Returns the number of elements in the map. O(1). + */ + function length(UintToAddressMap storage map) + internal + view + returns (uint256) + { + return _length(map._inner); + } + + /** + * @dev Returns the element stored at position `index` in the set. O(1). + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintToAddressMap storage map, uint256 index) + internal + view + returns (uint256, address) + { + (bytes32 key, bytes32 value) = _at(map._inner, index); + return (uint256(key), address(uint256(value))); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(UintToAddressMap storage map, uint256 key) + internal + view + returns (address) + { + return address(uint256(_get(map._inner, bytes32(key)))); + } + + /** + * @dev Same as {get}, with a custom error message when `key` is not in the map. + */ + function get( + UintToAddressMap storage map, + uint256 key, + string memory errorMessage + ) internal view returns (address) { + return address(uint256(_get(map._inner, bytes32(key), errorMessage))); + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/EnumerableSetUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/EnumerableSetUpgradeable.sol new file mode 100644 index 0000000..52c035b --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/EnumerableSetUpgradeable.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * + * ``` + * contract Example { + * // Add the library methods + * using EnumerableSet for EnumerableSet.AddressSet; + * + * // Declare a set state variable + * EnumerableSet.AddressSet private mySet; + * } + * ``` + * + * As of v3.0.0, only sets of type `address` (`AddressSet`) and `uint256` + * (`UintSet`) are supported. + */ +library EnumerableSetUpgradeable { + // To implement this library for multiple types with as little code + // repetition as possible, we write it in terms of a generic Set type with + // bytes32 values. + // The Set implementation uses private functions, and user-facing + // implementations (such as AddressSet) are just wrappers around the + // underlying Set. + // This means that we can only create new EnumerableSets for types that fit + // in bytes32. + + struct Set { + // Storage of set values + bytes32[] _values; + // Position of the value in the `values` array, plus 1 because index 0 + // means a value is not in the set. + mapping(bytes32 => uint256) _indexes; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function _add(Set storage set, bytes32 value) private returns (bool) { + if (!_contains(set, value)) { + set._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + set._indexes[value] = set._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function _remove(Set storage set, bytes32 value) private returns (bool) { + // We read and store the value's index to prevent multiple reads from the same storage slot + uint256 valueIndex = set._indexes[value]; + + if (valueIndex != 0) { + // Equivalent to contains(set, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 toDeleteIndex = valueIndex - 1; + uint256 lastIndex = set._values.length - 1; + + // When the value to delete is the last one, the swap operation is unnecessary. However, since this occurs + // so rarely, we still do the swap anyway to avoid the gas cost of adding an 'if' statement. + + bytes32 lastvalue = set._values[lastIndex]; + + // Move the last value to the index where the value to delete is + set._values[toDeleteIndex] = lastvalue; + // Update the index for the moved value + set._indexes[lastvalue] = toDeleteIndex + 1; // All indexes are 1-based + + // Delete the slot where the moved value was stored + set._values.pop(); + + // Delete the index for the deleted slot + delete set._indexes[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function _contains(Set storage set, bytes32 value) + private + view + returns (bool) + { + return set._indexes[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function _length(Set storage set) private view returns (uint256) { + return set._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function _at(Set storage set, uint256 index) + private + view + returns (bytes32) + { + require( + set._values.length > index, + "EnumerableSet: index out of bounds" + ); + return set._values[index]; + } + + // AddressSet + + struct AddressSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(AddressSet storage set, address value) + internal + returns (bool) + { + return _add(set._inner, bytes32(uint256(value))); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(AddressSet storage set, address value) + internal + returns (bool) + { + return _remove(set._inner, bytes32(uint256(value))); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(AddressSet storage set, address value) + internal + view + returns (bool) + { + return _contains(set._inner, bytes32(uint256(value))); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(AddressSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(AddressSet storage set, uint256 index) + internal + view + returns (address) + { + return address(uint256(_at(set._inner, index))); + } + + // UintSet + + struct UintSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(UintSet storage set, uint256 value) internal returns (bool) { + return _add(set._inner, bytes32(value)); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(UintSet storage set, uint256 value) + internal + returns (bool) + { + return _remove(set._inner, bytes32(value)); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(UintSet storage set, uint256 value) + internal + view + returns (bool) + { + return _contains(set._inner, bytes32(value)); + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(UintSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintSet storage set, uint256 index) + internal + view + returns (uint256) + { + return uint256(_at(set._inner, index)); + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol new file mode 100644 index 0000000..6af3fd9 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../GSN/ContextUpgradeable.sol"; +import "../proxy/Initializable.sol"; + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +contract PausableUpgradeable is Initializable, ContextUpgradeable { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + function __Pausable_init() internal initializer { + __Context_init_unchained(); + __Pausable_init_unchained(); + } + + function __Pausable_init_unchained() internal initializer { + _paused = false; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view returns (bool) { + return _paused; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + require(!_paused, "Pausable: paused"); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + require(_paused, "Pausable: not paused"); + _; + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } + + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/README.adoc b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/README.adoc new file mode 100644 index 0000000..3773c41 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/README.adoc @@ -0,0 +1,52 @@ += Utilities + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/utils + +Miscellaneous contracts and libraries containing utility functions you can use to improve security, work with new data types, or safely use low-level primitives. + +Security tools include: + + * {Pausable}: provides a simple way to halt activity in your contracts (often in response to an external threat). + * {ReentrancyGuard}: protects you from https://blog.openzeppelin.com/reentrancy-after-istanbul/[reentrant calls]. + +The {Address}, {Arrays} and {Strings} libraries provide more operations related to these native data types, while {SafeCast} adds ways to safely convert between the different signed and unsigned numeric types. + +For new data types: + + * {Counters}: a simple way to get a counter that can only be incremented or decremented. Very useful for ID generation, counting contract activity, among others. + * {EnumerableMap}: like Solidity's https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] type, but with key-value _enumeration_: this will let you know how many entries a mapping has, and iterate over them (which is not possible with `mapping`). + * {EnumerableSet}: like {EnumerableMap}, but for https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets]. Can be used to store privileged accounts, issued IDs, etc. + +[NOTE] +==== +Because Solidity does not support generic types, {EnumerableMap} and {EnumerableSet} are specialized to a limited number of key-value types. + +As of v3.0, {EnumerableMap} supports `uint256 -> address` (`UintToAddressMap`), and {EnumerableSet} supports `address` and `uint256` (`AddressSet` and `UintSet`). +==== + +Finally, {Create2} contains all necessary utilities to safely use the https://blog.openzeppelin.com/getting-the-most-out-of-create2/[`CREATE2` EVM opcode], without having to deal with low-level assembly. + +== Contracts + +{{Pausable}} + +{{ReentrancyGuard}} + +== Libraries + +{{Address}} + +{{Arrays}} + +{{Counters}} + +{{Create2}} + +{{EnumerableMap}} + +{{EnumerableSet}} + +{{SafeCast}} + +{{Strings}} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol new file mode 100644 index 0000000..f0cd44c --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; +import "../proxy/Initializable.sol"; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +contract ReentrancyGuardUpgradeable is Initializable { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + function __ReentrancyGuard_init() internal initializer { + __ReentrancyGuard_init_unchained(); + } + + function __ReentrancyGuard_init_unchained() internal initializer { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and make it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } + uint256[49] private __gap; +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/SafeCastUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/SafeCastUpgradeable.sol new file mode 100644 index 0000000..46b4e03 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/SafeCastUpgradeable.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow + * checks. + * + * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can + * easily result in undesired exploitation or bugs, since developers usually + * assume that overflows raise errors. `SafeCast` restores this intuition by + * reverting the transaction when such an operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + * + * Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing + * all math on `uint256` and `int256` and then downcasting. + */ +library SafeCastUpgradeable { + /** + * @dev Returns the downcasted uint128 from uint256, reverting on + * overflow (when the input is greater than largest uint128). + * + * Counterpart to Solidity's `uint128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + */ + function toUint128(uint256 value) internal pure returns (uint128) { + require(value < 2**128, "SafeCast: value doesn't fit in 128 bits"); + return uint128(value); + } + + /** + * @dev Returns the downcasted uint64 from uint256, reverting on + * overflow (when the input is greater than largest uint64). + * + * Counterpart to Solidity's `uint64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + */ + function toUint64(uint256 value) internal pure returns (uint64) { + require(value < 2**64, "SafeCast: value doesn't fit in 64 bits"); + return uint64(value); + } + + /** + * @dev Returns the downcasted uint32 from uint256, reverting on + * overflow (when the input is greater than largest uint32). + * + * Counterpart to Solidity's `uint32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + */ + function toUint32(uint256 value) internal pure returns (uint32) { + require(value < 2**32, "SafeCast: value doesn't fit in 32 bits"); + return uint32(value); + } + + /** + * @dev Returns the downcasted uint16 from uint256, reverting on + * overflow (when the input is greater than largest uint16). + * + * Counterpart to Solidity's `uint16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + */ + function toUint16(uint256 value) internal pure returns (uint16) { + require(value < 2**16, "SafeCast: value doesn't fit in 16 bits"); + return uint16(value); + } + + /** + * @dev Returns the downcasted uint8 from uint256, reverting on + * overflow (when the input is greater than largest uint8). + * + * Counterpart to Solidity's `uint8` operator. + * + * Requirements: + * + * - input must fit into 8 bits. + */ + function toUint8(uint256 value) internal pure returns (uint8) { + require(value < 2**8, "SafeCast: value doesn't fit in 8 bits"); + return uint8(value); + } + + /** + * @dev Converts a signed int256 into an unsigned uint256. + * + * Requirements: + * + * - input must be greater than or equal to 0. + */ + function toUint256(int256 value) internal pure returns (uint256) { + require(value >= 0, "SafeCast: value must be positive"); + return uint256(value); + } + + /** + * @dev Returns the downcasted int128 from int256, reverting on + * overflow (when the input is less than smallest int128 or + * greater than largest int128). + * + * Counterpart to Solidity's `int128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + * + * _Available since v3.1._ + */ + function toInt128(int256 value) internal pure returns (int128) { + require( + value >= -2**127 && value < 2**127, + "SafeCast: value doesn't fit in 128 bits" + ); + return int128(value); + } + + /** + * @dev Returns the downcasted int64 from int256, reverting on + * overflow (when the input is less than smallest int64 or + * greater than largest int64). + * + * Counterpart to Solidity's `int64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + * + * _Available since v3.1._ + */ + function toInt64(int256 value) internal pure returns (int64) { + require( + value >= -2**63 && value < 2**63, + "SafeCast: value doesn't fit in 64 bits" + ); + return int64(value); + } + + /** + * @dev Returns the downcasted int32 from int256, reverting on + * overflow (when the input is less than smallest int32 or + * greater than largest int32). + * + * Counterpart to Solidity's `int32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + * + * _Available since v3.1._ + */ + function toInt32(int256 value) internal pure returns (int32) { + require( + value >= -2**31 && value < 2**31, + "SafeCast: value doesn't fit in 32 bits" + ); + return int32(value); + } + + /** + * @dev Returns the downcasted int16 from int256, reverting on + * overflow (when the input is less than smallest int16 or + * greater than largest int16). + * + * Counterpart to Solidity's `int16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + * + * _Available since v3.1._ + */ + function toInt16(int256 value) internal pure returns (int16) { + require( + value >= -2**15 && value < 2**15, + "SafeCast: value doesn't fit in 16 bits" + ); + return int16(value); + } + + /** + * @dev Returns the downcasted int8 from int256, reverting on + * overflow (when the input is less than smallest int8 or + * greater than largest int8). + * + * Counterpart to Solidity's `int8` operator. + * + * Requirements: + * + * - input must fit into 8 bits. + * + * _Available since v3.1._ + */ + function toInt8(int256 value) internal pure returns (int8) { + require( + value >= -2**7 && value < 2**7, + "SafeCast: value doesn't fit in 8 bits" + ); + return int8(value); + } + + /** + * @dev Converts an unsigned uint256 into a signed int256. + * + * Requirements: + * + * - input must be less than or equal to maxInt256. + */ + function toInt256(uint256 value) internal pure returns (int256) { + require(value < 2**255, "SafeCast: value doesn't fit in an int256"); + return int256(value); + } +} diff --git a/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol new file mode 100644 index 0000000..4f396b8 --- /dev/null +++ b/protocol/contracts/temp/openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @dev String operations. + */ +library StringsUpgradeable { + /** + * @dev Converts a `uint256` to its ASCII `string` representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT licence + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + uint256 index = digits - 1; + temp = value; + while (temp != 0) { + buffer[index--] = bytes1(uint8(48 + (temp % 10))); + temp /= 10; + } + return string(buffer); + } +} diff --git a/protocol/contracts/test/Token.sol b/protocol/contracts/test/Token.sol new file mode 100644 index 0000000..ed7051b --- /dev/null +++ b/protocol/contracts/test/Token.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20Detailed.sol"; + +contract Token is ERC20, ERC20Detailed { + constructor() public ERC20Detailed("stake dao test token", "TEST", 18) { + _mint(msg.sender, 30000 * 10**18); + } +} diff --git a/protocol/contracts/tokens/CRV.vy b/protocol/contracts/tokens/CRV.vy new file mode 100644 index 0000000..2b9c3f3 --- /dev/null +++ b/protocol/contracts/tokens/CRV.vy @@ -0,0 +1,374 @@ +# @version 0.2.7 +""" +@title Curve DAO Token +@author Curve Finance +@license MIT +@notice ERC20 with piecewise-linear mining supply. +@dev Based on the ERC-20 token standard as defined at + https://eips.ethereum.org/EIPS/eip-20 +""" + +from vyper.interfaces import ERC20 + +implements: ERC20 + + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + +event UpdateMiningParameters: + time: uint256 + rate: uint256 + supply: uint256 + +event SetMinter: + minter: address + +event SetAdmin: + admin: address + + +name: public(String[64]) +symbol: public(String[32]) +decimals: public(uint256) + +balanceOf: public(HashMap[address, uint256]) +allowances: HashMap[address, HashMap[address, uint256]] +total_supply: uint256 + +minter: public(address) +admin: public(address) + +# General constants +YEAR: constant(uint256) = 86400 * 365 + +# Allocation: +# ========= +# * shareholders - 30% +# * emplyees - 3% +# * DAO-controlled reserve - 5% +# * Early users - 5% +# == 43% == +# left for inflation: 57% + +# Supply parameters +INITIAL_SUPPLY: constant(uint256) = 1_303_030_303 +INITIAL_RATE: constant(uint256) = 274_815_283 * 10 ** 18 / YEAR # leading to 43% premine +RATE_REDUCTION_TIME: constant(uint256) = YEAR +RATE_REDUCTION_COEFFICIENT: constant(uint256) = 1189207115002721024 # 2 ** (1/4) * 1e18 +RATE_DENOMINATOR: constant(uint256) = 10 ** 18 +INFLATION_DELAY: constant(uint256) = 86400 + +# Supply variables +mining_epoch: public(int128) +start_epoch_time: public(uint256) +rate: public(uint256) + +start_epoch_supply: uint256 + + +@external +def __init__(_name: String[64], _symbol: String[32], _decimals: uint256): + """ + @notice Contract constructor + @param _name Token full name + @param _symbol Token symbol + @param _decimals Number of decimals for token + """ + init_supply: uint256 = INITIAL_SUPPLY * 10 ** _decimals + self.name = _name + self.symbol = _symbol + self.decimals = _decimals + self.balanceOf[msg.sender] = init_supply + self.total_supply = init_supply + self.admin = msg.sender + log Transfer(ZERO_ADDRESS, msg.sender, init_supply) + + self.start_epoch_time = block.timestamp + INFLATION_DELAY - RATE_REDUCTION_TIME + self.mining_epoch = -1 + self.rate = 0 + self.start_epoch_supply = init_supply + + +@internal +def _update_mining_parameters(): + """ + @dev Update mining rate and supply at the start of the epoch + Any modifying mining call must also call this + """ + _rate: uint256 = self.rate + _start_epoch_supply: uint256 = self.start_epoch_supply + + self.start_epoch_time += RATE_REDUCTION_TIME + self.mining_epoch += 1 + + if _rate == 0: + _rate = INITIAL_RATE + else: + _start_epoch_supply += _rate * RATE_REDUCTION_TIME + self.start_epoch_supply = _start_epoch_supply + _rate = _rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT + + self.rate = _rate + + log UpdateMiningParameters(block.timestamp, _rate, _start_epoch_supply) + + +@external +def update_mining_parameters(): + """ + @notice Update mining rate and supply at the start of the epoch + @dev Callable by any address, but only once per epoch + Total supply becomes slightly larger if this function is called late + """ + assert block.timestamp >= self.start_epoch_time + RATE_REDUCTION_TIME # dev: too soon! + self._update_mining_parameters() + + +@external +def start_epoch_time_write() -> uint256: + """ + @notice Get timestamp of the current mining epoch start + while simultaneously updating mining parameters + @return Timestamp of the epoch + """ + _start_epoch_time: uint256 = self.start_epoch_time + if block.timestamp >= _start_epoch_time + RATE_REDUCTION_TIME: + self._update_mining_parameters() + return self.start_epoch_time + else: + return _start_epoch_time + + +@external +def future_epoch_time_write() -> uint256: + """ + @notice Get timestamp of the next mining epoch start + while simultaneously updating mining parameters + @return Timestamp of the next epoch + """ + _start_epoch_time: uint256 = self.start_epoch_time + if block.timestamp >= _start_epoch_time + RATE_REDUCTION_TIME: + self._update_mining_parameters() + return self.start_epoch_time + RATE_REDUCTION_TIME + else: + return _start_epoch_time + RATE_REDUCTION_TIME + + +@internal +@view +def _available_supply() -> uint256: + return self.start_epoch_supply + (block.timestamp - self.start_epoch_time) * self.rate + + +@external +@view +def available_supply() -> uint256: + """ + @notice Current number of tokens in existence (claimed or unclaimed) + """ + return self._available_supply() + + +@external +@view +def mintable_in_timeframe(start: uint256, end: uint256) -> uint256: + """ + @notice How much supply is mintable from start timestamp till end timestamp + @param start Start of the time interval (timestamp) + @param end End of the time interval (timestamp) + @return Tokens mintable from `start` till `end` + """ + assert start <= end # dev: start > end + to_mint: uint256 = 0 + current_epoch_time: uint256 = self.start_epoch_time + current_rate: uint256 = self.rate + + # Special case if end is in future (not yet minted) epoch + if end > current_epoch_time + RATE_REDUCTION_TIME: + current_epoch_time += RATE_REDUCTION_TIME + current_rate = current_rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT + + assert end <= current_epoch_time + RATE_REDUCTION_TIME # dev: too far in future + + for i in range(999): # Curve will not work in 1000 years. Darn! + if end >= current_epoch_time: + current_end: uint256 = end + if current_end > current_epoch_time + RATE_REDUCTION_TIME: + current_end = current_epoch_time + RATE_REDUCTION_TIME + + current_start: uint256 = start + if current_start >= current_epoch_time + RATE_REDUCTION_TIME: + break # We should never get here but what if... + elif current_start < current_epoch_time: + current_start = current_epoch_time + + to_mint += current_rate * (current_end - current_start) + + if start >= current_epoch_time: + break + + current_epoch_time -= RATE_REDUCTION_TIME + current_rate = current_rate * RATE_REDUCTION_COEFFICIENT / RATE_DENOMINATOR # double-division with rounding made rate a bit less => good + assert current_rate <= INITIAL_RATE # This should never happen + + return to_mint + + +@external +def set_minter(_minter: address): + """ + @notice Set the minter address + @dev Only callable once, when minter has not yet been set + @param _minter Address of the minter + """ + assert msg.sender == self.admin # dev: admin only + assert self.minter == ZERO_ADDRESS # dev: can set the minter only once, at creation + self.minter = _minter + log SetMinter(_minter) + + +@external +def set_admin(_admin: address): + """ + @notice Set the new admin. + @dev After all is set up, admin only can change the token name + @param _admin New admin address + """ + assert msg.sender == self.admin # dev: admin only + self.admin = _admin + log SetAdmin(_admin) + + +@external +@view +def totalSupply() -> uint256: + """ + @notice Total number of tokens in existence. + """ + return self.total_supply + + +@external +@view +def allowance(_owner : address, _spender : address) -> uint256: + """ + @notice Check the amount of tokens that an owner allowed to a spender + @param _owner The address which owns the funds + @param _spender The address which will spend the funds + @return uint256 specifying the amount of tokens still available for the spender + """ + return self.allowances[_owner][_spender] + + +@external +def transfer(_to : address, _value : uint256) -> bool: + """ + @notice Transfer `_value` tokens from `msg.sender` to `_to` + @dev Vyper does not allow underflows, so the subtraction in + this function will revert on an insufficient balance + @param _to The address to transfer to + @param _value The amount to be transferred + @return bool success + """ + assert _to != ZERO_ADDRESS # dev: transfers to 0x0 are not allowed + self.balanceOf[msg.sender] -= _value + self.balanceOf[_to] += _value + log Transfer(msg.sender, _to, _value) + return True + + +@external +def transferFrom(_from : address, _to : address, _value : uint256) -> bool: + """ + @notice Transfer `_value` tokens from `_from` to `_to` + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + @return bool success + """ + assert _to != ZERO_ADDRESS # dev: transfers to 0x0 are not allowed + # NOTE: vyper does not allow underflows + # so the following subtraction would revert on insufficient balance + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + self.allowances[_from][msg.sender] -= _value + log Transfer(_from, _to, _value) + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @notice Approve `_spender` to transfer `_value` tokens on behalf of `msg.sender` + @dev Approval may only be from zero -> nonzero or from nonzero -> zero in order + to mitigate the potential race condition described here: + https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will spend the funds + @param _value The amount of tokens to be spent + @return bool success + """ + assert _value == 0 or self.allowances[msg.sender][_spender] == 0 + self.allowances[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + return True + + +@external +def mint(_to: address, _value: uint256) -> bool: + """ + @notice Mint `_value` tokens and assign them to `_to` + @dev Emits a Transfer event originating from 0x00 + @param _to The account that will receive the created tokens + @param _value The amount that will be created + @return bool success + """ + assert msg.sender == self.minter # dev: minter only + assert _to != ZERO_ADDRESS # dev: zero address + + if block.timestamp >= self.start_epoch_time + RATE_REDUCTION_TIME: + self._update_mining_parameters() + + _total_supply: uint256 = self.total_supply + _value + assert _total_supply <= self._available_supply() # dev: exceeds allowable mint amount + self.total_supply = _total_supply + + self.balanceOf[_to] += _value + log Transfer(ZERO_ADDRESS, _to, _value) + + return True + + +@external +def burn(_value: uint256) -> bool: + """ + @notice Burn `_value` tokens belonging to `msg.sender` + @dev Emits a Transfer event with a destination of 0x00 + @param _value The amount that will be burned + @return bool success + """ + self.balanceOf[msg.sender] -= _value + self.total_supply -= _value + + log Transfer(msg.sender, ZERO_ADDRESS, _value) + return True + + +@external +def set_name(_name: String[64], _symbol: String[32]): + """ + @notice Change the token name and symbol to `_name` and `_symbol` + @dev Only callable by the admin account + @param _name New token name + @param _symbol New token symbol + """ + assert msg.sender == self.admin, "Only admin is allowed to change name" + self.name = _name + self.symbol = _symbol \ No newline at end of file diff --git a/protocol/contracts/tokens/VaultToken.vy b/protocol/contracts/tokens/VaultToken.vy new file mode 100644 index 0000000..70b3ab7 --- /dev/null +++ b/protocol/contracts/tokens/VaultToken.vy @@ -0,0 +1,166 @@ +# @version 0.2.4 +# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md + +from vyper.interfaces import ERC20 + +implements: ERC20 + +interface Curve: + def owner() -> address: view + + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + +name: public(String[64]) +symbol: public(String[32]) +decimals: public(uint256) + +# NOTE: By declaring `balanceOf` as public, vyper automatically generates a 'balanceOf()' getter +# method to allow access to account balances. +# The _KeyType will become a required parameter for the getter and it will return _ValueType. +# See: https://vyper.readthedocs.io/en/v0.1.0-beta.8/types.html?highlight=getter#mappings +balanceOf: public(HashMap[address, uint256]) +allowances: HashMap[address, HashMap[address, uint256]] +total_supply: uint256 +minter: address + + +@external +def __init__(_name: String[64], _symbol: String[32], _decimals: uint256, _supply: uint256): + init_supply: uint256 = _supply * 10 ** _decimals + self.name = _name + self.symbol = _symbol + self.decimals = _decimals + self.balanceOf[msg.sender] = init_supply + self.total_supply = init_supply + self.minter = msg.sender + log Transfer(ZERO_ADDRESS, msg.sender, init_supply) + + +@external +def set_minter(_minter: address): + assert msg.sender == self.minter + self.minter = _minter + + +@external +def set_name(_name: String[64], _symbol: String[32]): + assert Curve(self.minter).owner() == msg.sender + self.name = _name + self.symbol = _symbol + + +@view +@external +def totalSupply() -> uint256: + """ + @dev Total number of tokens in existence. + """ + return self.total_supply + + +@view +@external +def allowance(_owner : address, _spender : address) -> uint256: + """ + @dev Function to check the amount of tokens that an owner allowed to a spender. + @param _owner The address which owns the funds. + @param _spender The address which will spend the funds. + @return An uint256 specifying the amount of tokens still available for the spender. + """ + return self.allowances[_owner][_spender] + + +@external +def transfer(_to : address, _value : uint256) -> bool: + """ + @dev Transfer token for a specified address + @param _to The address to transfer to. + @param _value The amount to be transferred. + """ + # NOTE: vyper does not allow underflows + # so the following subtraction would revert on insufficient balance + self.balanceOf[msg.sender] -= _value + self.balanceOf[_to] += _value + log Transfer(msg.sender, _to, _value) + return True + + +@external +def transferFrom(_from : address, _to : address, _value : uint256) -> bool: + """ + @dev Transfer tokens from one address to another. + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + """ + # NOTE: vyper does not allow underflows + # so the following subtraction would revert on insufficient balance + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + if msg.sender != self.minter: # minter is allowed to transfer anything + # NOTE: vyper does not allow underflows + # so the following subtraction would revert on insufficient allowance + self.allowances[_from][msg.sender] -= _value + log Transfer(_from, _to, _value) + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. + Beware that changing an allowance with this method brings the risk that someone may use both the old + and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this + race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: + https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will spend the funds. + @param _value The amount of tokens to be spent. + """ + assert _value == 0 or self.allowances[msg.sender][_spender] == 0 + self.allowances[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + return True + + +@external +def mint(_to: address, _value: uint256) -> bool: + """ + @dev Mint an amount of the token and assigns it to an account. + This encapsulates the modification of balances such that the + proper events are emitted. + @param _to The account that will receive the created tokens. + @param _value The amount that will be created. + """ + assert msg.sender == self.minter + assert _to != ZERO_ADDRESS + self.total_supply += _value + self.balanceOf[_to] += _value + log Transfer(ZERO_ADDRESS, _to, _value) + return True + + +@external +def burnFrom(_to: address, _value: uint256) -> bool: + """ + @dev Burn an amount of the token from a given account. + @param _to The account whose tokens will be burned. + @param _value The amount that will be burned. + """ + assert msg.sender == self.minter + assert _to != ZERO_ADDRESS + + self.total_supply -= _value + self.balanceOf[_to] -= _value + log Transfer(_to, ZERO_ADDRESS, _value) + + return True \ No newline at end of file diff --git a/protocol/contracts/treasury/TreasuryVault.sol b/protocol/contracts/treasury/TreasuryVault.sol new file mode 100644 index 0000000..4bf941b --- /dev/null +++ b/protocol/contracts/treasury/TreasuryVault.sol @@ -0,0 +1,155 @@ +/** + *Submitted for verification at Etherscan.io on 2020-08-23 + */ + +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.16 <0.7.0; + +import "../temp/openzeppelin/IERC20.sol"; +import "../temp/openzeppelin/SafeMath.sol"; +import "../temp/openzeppelin/Address.sol"; +import "../temp/openzeppelin/SafeERC20.sol"; + +import "../../interfaces/yearn/IOneSplitAudit.sol"; + +interface Governance { + function notifyRewardAmount(uint256) external; +} + +interface TreasuryZap { + function swap( + address token_in, + address token_out, + uint256 amount_in + ) external returns (uint256 amount_out); +} + +contract TreasuryVault { + using SafeERC20 for IERC20; + + address public governance; + address public onesplit; + address public rewards; + address public governanceStaking; + address public treasuryZap; + + mapping(address => bool) authorized; + + constructor( + address _governanceStaking, + address _rewards, + address _treasuryZap + ) public { + governance = msg.sender; + onesplit = address(0x50FDA034C0Ce7a8f7EFDAebDA7Aa7cA21CC1267e); + treasuryZap = address(_treasuryZap); + governanceStaking = address(_governanceStaking); + rewards = address(_rewards); + } + + function setOnesplit(address _onesplit) external { + require(msg.sender == governance, "!governance"); + onesplit = _onesplit; + } + + function setTreasuryZap(address _treasuryZap) external { + require(msg.sender == governance, "!governance"); + treasuryZap = _treasuryZap; + } + + function setRewards(address _rewards) external { + require(msg.sender == governance, "!governance"); + rewards = _rewards; + } + + function setGovernanceStaking(address _governanceStaking) external { + require(msg.sender == governance, "!governance"); + governanceStaking = _governanceStaking; + } + + function setAuthorized(address _authorized) external { + require(msg.sender == governance, "!governance"); + authorized[_authorized] = true; + } + + function revokeAuthorized(address _authorized) external { + require(msg.sender == governance, "!governance"); + authorized[_authorized] = false; + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function toGovernance(address _token, uint256 _amount) external { + require(msg.sender == governance, "!governance"); + IERC20(_token).safeTransfer(governance, _amount); + } + + function toGovernanceStaking() external { + require(authorized[msg.sender] == true, "!authorized"); + uint256 _balance = IERC20(rewards).balanceOf(address(this)); + IERC20(rewards).safeApprove(governanceStaking, 0); + IERC20(rewards).safeApprove(governanceStaking, _balance); + Governance(governanceStaking).notifyRewardAmount(_balance); + } + + function getExpectedReturn( + address _from, + address _to, + uint256 parts + ) external view returns (uint256 expected) { + uint256 _balance = IERC20(_from).balanceOf(address(this)); + (expected, ) = IOneSplitAudit(onesplit).getExpectedReturn( + _from, + _to, + _balance, + parts, + 0 + ); + } + + // Only allows to withdraw non-core strategy tokens ~ this is over and above normal yield + function convert( + address _from, + address _to, + uint256 parts + ) external { + require(authorized[msg.sender] == true, "!authorized"); + uint256 _amount = IERC20(_from).balanceOf(address(this)); + uint256[] memory _distribution; + uint256 _expected; + IERC20(_from).safeApprove(onesplit, 0); + IERC20(_from).safeApprove(onesplit, _amount); + (_expected, _distribution) = IOneSplitAudit(onesplit).getExpectedReturn( + _from, + _to, + _amount, + parts, + 0 + ); + IOneSplitAudit(onesplit).swap( + _from, + _to, + _amount, + _expected, + _distribution, + 0 + ); + } + + function swapViaZap( + address token_in, + address token_out, + uint256 amount_in + ) public returns (uint256 amount_out) { + require(authorized[msg.sender] == true, "!authorized"); + uint256 _balance = IERC20(token_in).balanceOf(address(this)); + require(_balance >= amount_in, "!balance"); + IERC20(token_in).safeApprove(treasuryZap, 0); + IERC20(token_in).safeApprove(treasuryZap, amount_in); + return TreasuryZap(treasuryZap).swap(token_in, token_out, amount_in); + } +} diff --git a/protocol/contracts/treasury/TreasuryZap.sol b/protocol/contracts/treasury/TreasuryZap.sol new file mode 100644 index 0000000..1b81f5d --- /dev/null +++ b/protocol/contracts/treasury/TreasuryZap.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: AGPL +pragma solidity =0.6.12; + +import "../temp/openzeppelin/IERC20.sol"; +import "../temp/openzeppelin/SafeMath.sol"; +import "../temp/openzeppelin/Address.sol"; +import "../temp/openzeppelin/SafeERC20.sol"; +import "../temp/openzeppelin/Ownable.sol"; + + +interface CurveRegistry { + function get_pool_from_lp_token(address) external view returns (address); +} + +interface Uniswap { + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) + external + returns ( + uint256 amountA, + uint256 amountB, + uint256 liquidity + ); + + function getAmountsOut(uint256 amountIn, address[] memory path) + external + view + returns (uint256[] memory amounts); +} + +interface Zapper { + function ZapOut( + address payable toWhomToIssue, + address swapAddress, + uint256 incomingCrv, + address toToken, + uint256 minToTokens + ) external returns (uint256 ToTokensBought); +} + +contract TreasuryZap is Ownable { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + uint256 public minAmount; + + address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant uniswap = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address constant curve_registry = 0x7D86446dDb609eD0F5f8684AcF30380a356b2B4c; + address constant curve_zap_out = 0xA3061Cf6aC1423c6F40917AD49602cBA187181Dc; + mapping(address => address) curve_deposit; + + constructor() public { + curve_deposit[0x845838DF265Dcd2c412A1Dc9e959c7d08537f8a2] = 0xeB21209ae4C2c9FF2a86ACA31E123764A3B6Bc06; // compound + curve_deposit[0x9fC689CCaDa600B6DF723D9E47D84d76664a1F23] = 0xac795D2c97e60DF6a99ff1c814727302fD747a80; // usdt + curve_deposit[0xdF5e0e81Dff6FAF3A7e52BA697820c5e32D806A8] = 0xbBC81d23Ea2c3ec7e56D39296F0cbB648873a5d3; // y + curve_deposit[0x3B3Ac5386837Dc563660FB6a0937DFAa5924333B] = 0xb6c057591E073249F2D9D88Ba59a46CFC9B59EdB; // busd + curve_deposit[0xC25a3A3b969415c80451098fa907EC722572917F] = 0xFCBa3E75865d2d561BE8D220616520c171F12851; // susdv2 + curve_deposit[0xD905e2eaeBe188fc92179b6350807D8bd91Db0D8] = 0xA50cCc70b6a011CffDdf45057E39679379187287; // pax + } + + function setMinAmount(uint256 _minAmount) external onlyOwner { + minAmount = _minAmount; + } + + function swap( + address token_in, + address token_out, + uint256 amount_in + ) public returns (uint256 amount_out) { + IERC20(token_in).safeTransferFrom(msg.sender, address(this), amount_in); + address pool_in = token_to_curve_pool(token_in); + if (pool_in != address(0)) { + amount_out = swap_curve(token_in, token_out, amount_in); + } else { + amount_out = swap_uniswap(token_in, token_out, amount_in); + } + } + + function token_to_curve_pool(address token_in) + internal + returns (address pool) + { + pool = CurveRegistry(curve_registry).get_pool_from_lp_token(token_in); + if (curve_deposit[token_in] != address(0)) + pool = curve_deposit[token_in]; + } + + function swap_curve( + address token_in, + address token_out, + uint256 amount_in + ) public returns (uint256 amount_out) { + IERC20(token_in).safeApprove(curve_zap_out, amount_in); + address pool_in = token_to_curve_pool(token_in); + amount_out = Zapper(curve_zap_out).ZapOut( + payable(msg.sender), + pool_in, + amount_in, + token_out, + 0 + ); + } + + function swap_uniswap( + address token_in, + address token_out, + uint256 amount_in + ) public returns (uint256 amount_out) { + if (token_in == token_out) return amount_in; + bool is_weth = token_in == weth || token_out == weth; + address[] memory path = new address[](is_weth ? 2 : 3); + path[0] = token_in; + if (is_weth) { + path[1] = token_out; + } else { + path[1] = weth; + path[2] = token_out; + } + if (IERC20(token_in).allowance(address(this), uniswap) < amount_in) + IERC20(token_in).safeApprove(uniswap, type(uint256).max); + uint256[] memory amounts = Uniswap(uniswap).swapExactTokensForTokens( + amount_in, + minAmount, + path, + msg.sender, + block.timestamp + ); + amount_out = amounts[amounts.length - 1]; + } +} diff --git a/protocol/contracts/usdc/FiatTokenV2_1.sol b/protocol/contracts/usdc/FiatTokenV2_1.sol new file mode 100644 index 0000000..0956320 --- /dev/null +++ b/protocol/contracts/usdc/FiatTokenV2_1.sol @@ -0,0 +1,1569 @@ +/** + *Submitted for verification at Etherscan.io on 2021-04-17 + */ + +import "../temp/openzeppelin/SafeMath.sol"; +import "../temp/openzeppelin/Address.sol"; +import "../temp/openzeppelin/SafeERC20.sol"; +import "../temp/openzeppelin/Ownable.sol"; + +// File: contracts/v1/AbstractFiatTokenV1.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +abstract contract AbstractFiatTokenV1 is IERC20 { + function _approve( + address owner, + address spender, + uint256 value + ) internal virtual; + + function _transfer( + address from, + address to, + uint256 value + ) internal virtual; +} + +// File: contracts/v1/Pausable.sol + +/** + * Copyright (c) 2016 Smart Contract Solutions, Inc. + * Copyright (c) 2018-2020 CENTRE SECZ0 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +/** + * @notice Base contract which allows children to implement an emergency stop + * mechanism + * @dev Forked from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/feb665136c0dae9912e08397c1a21c4af3651ef3/contracts/lifecycle/Pausable.sol + * Modifications: + * 1. Added pauser role, switched pause/unpause to be onlyPauser (6/14/2018) + * 2. Removed whenNotPause/whenPaused from pause/unpause (6/14/2018) + * 3. Removed whenPaused (6/14/2018) + * 4. Switches ownable library to use ZeppelinOS (7/12/18) + * 5. Remove constructor (7/13/18) + * 6. Reformat, conform to Solidity 0.6 syntax and add error messages (5/13/20) + * 7. Make public functions external (5/27/20) + */ +contract Pausable is Ownable { + event Pause(); + event Unpause(); + event PauserChanged(address indexed newAddress); + + address public pauser; + bool public paused = false; + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + */ + modifier whenNotPaused() { + require(!paused, "Pausable: paused"); + _; + } + + /** + * @dev throws if called by any account other than the pauser + */ + modifier onlyPauser() { + require(msg.sender == pauser, "Pausable: caller is not the pauser"); + _; + } + + /** + * @dev called by the owner to pause, triggers stopped state + */ + function pause() external onlyPauser { + paused = true; + emit Pause(); + } + + /** + * @dev called by the owner to unpause, returns to normal state + */ + function unpause() external onlyPauser { + paused = false; + emit Unpause(); + } + + /** + * @dev update the pauser role + */ + function updatePauser(address _newPauser) external onlyOwner { + require( + _newPauser != address(0), + "Pausable: new pauser is the zero address" + ); + pauser = _newPauser; + emit PauserChanged(pauser); + } +} + +// File: contracts/v1/Blacklistable.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +/** + * @title Blacklistable Token + * @dev Allows accounts to be blacklisted by a "blacklister" role + */ +contract Blacklistable is Ownable { + address public blacklister; + mapping(address => bool) internal blacklisted; + + event Blacklisted(address indexed _account); + event UnBlacklisted(address indexed _account); + event BlacklisterChanged(address indexed newBlacklister); + + /** + * @dev Throws if called by any account other than the blacklister + */ + modifier onlyBlacklister() { + require( + msg.sender == blacklister, + "Blacklistable: caller is not the blacklister" + ); + _; + } + + /** + * @dev Throws if argument account is blacklisted + * @param _account The address to check + */ + modifier notBlacklisted(address _account) { + require( + !blacklisted[_account], + "Blacklistable: account is blacklisted" + ); + _; + } + + /** + * @dev Checks if account is blacklisted + * @param _account The address to check + */ + function isBlacklisted(address _account) external view returns (bool) { + return blacklisted[_account]; + } + + /** + * @dev Adds account to blacklist + * @param _account The address to blacklist + */ + function blacklist(address _account) external onlyBlacklister { + blacklisted[_account] = true; + emit Blacklisted(_account); + } + + /** + * @dev Removes account from blacklist + * @param _account The address to remove from the blacklist + */ + function unBlacklist(address _account) external onlyBlacklister { + blacklisted[_account] = false; + emit UnBlacklisted(_account); + } + + function updateBlacklister(address _newBlacklister) external onlyOwner { + require( + _newBlacklister != address(0), + "Blacklistable: new blacklister is the zero address" + ); + blacklister = _newBlacklister; + emit BlacklisterChanged(blacklister); + } +} + +// File: contracts/v1/FiatTokenV1.sol + +/** + * + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +/** + * @title FiatToken + * @dev ERC20 Token backed by fiat reserves + */ +contract FiatTokenV1 is AbstractFiatTokenV1, Ownable, Pausable, Blacklistable { + using SafeMath for uint256; + + string public name; + string public symbol; + uint8 public decimals; + string public currency; + address public masterMinter; + bool internal initialized; + + mapping(address => uint256) internal balances; + mapping(address => mapping(address => uint256)) internal allowed; + uint256 internal totalSupply_ = 0; + mapping(address => bool) internal minters; + mapping(address => uint256) internal minterAllowed; + + event Mint(address indexed minter, address indexed to, uint256 amount); + event Burn(address indexed burner, uint256 amount); + event MinterConfigured(address indexed minter, uint256 minterAllowedAmount); + event MinterRemoved(address indexed oldMinter); + event MasterMinterChanged(address indexed newMasterMinter); + + function initialize( + string memory tokenName, + string memory tokenSymbol, + string memory tokenCurrency, + uint8 tokenDecimals, + address newMasterMinter, + address newPauser, + address newBlacklister, + address newOwner + ) public { + require(!initialized, "FiatToken: contract is already initialized"); + require( + newMasterMinter != address(0), + "FiatToken: new masterMinter is the zero address" + ); + require( + newPauser != address(0), + "FiatToken: new pauser is the zero address" + ); + require( + newBlacklister != address(0), + "FiatToken: new blacklister is the zero address" + ); + require( + newOwner != address(0), + "FiatToken: new owner is the zero address" + ); + + name = tokenName; + symbol = tokenSymbol; + currency = tokenCurrency; + decimals = tokenDecimals; + masterMinter = newMasterMinter; + pauser = newPauser; + blacklister = newBlacklister; + // setOwner(newOwner); + initialized = true; + } + + /** + * @dev Throws if called by any account other than a minter + */ + modifier onlyMinters() { + require(minters[msg.sender], "FiatToken: caller is not a minter"); + _; + } + + /** + * @dev Function to mint tokens + * @param _to The address that will receive the minted tokens. + * @param _amount The amount of tokens to mint. Must be less than or equal + * to the minterAllowance of the caller. + * @return A boolean that indicates if the operation was successful. + */ + function mint(address _to, uint256 _amount) + external + whenNotPaused + onlyMinters + notBlacklisted(msg.sender) + notBlacklisted(_to) + returns (bool) + { + require(_to != address(0), "FiatToken: mint to the zero address"); + require(_amount > 0, "FiatToken: mint amount not greater than 0"); + + uint256 mintingAllowedAmount = minterAllowed[msg.sender]; + require( + _amount <= mintingAllowedAmount, + "FiatToken: mint amount exceeds minterAllowance" + ); + + totalSupply_ = totalSupply_.add(_amount); + balances[_to] = balances[_to].add(_amount); + minterAllowed[msg.sender] = mintingAllowedAmount.sub(_amount); + emit Mint(msg.sender, _to, _amount); + emit Transfer(address(0), _to, _amount); + return true; + } + + /** + * @dev Throws if called by any account other than the masterMinter + */ + modifier onlyMasterMinter() { + require( + msg.sender == masterMinter, + "FiatToken: caller is not the masterMinter" + ); + _; + } + + /** + * @dev Get minter allowance for an account + * @param minter The address of the minter + */ + function minterAllowance(address minter) external view returns (uint256) { + return minterAllowed[minter]; + } + + /** + * @dev Checks if account is a minter + * @param account The address to check + */ + function isMinter(address account) external view returns (bool) { + return minters[account]; + } + + /** + * @notice Amount of remaining tokens spender is allowed to transfer on + * behalf of the token owner + * @param owner Token owner's address + * @param spender Spender's address + * @return Allowance amount + */ + function allowance(address owner, address spender) + external + view + override + returns (uint256) + { + return allowed[owner][spender]; + } + + /** + * @dev Get totalSupply of token + */ + function totalSupply() external view override returns (uint256) { + return totalSupply_; + } + + /** + * @dev Get token balance of an account + * @param account address The account + */ + function balanceOf(address account) + external + view + override + returns (uint256) + { + return balances[account]; + } + + /** + * @notice Set spender's allowance over the caller's tokens to be a given + * value. + * @param spender Spender's address + * @param value Allowance amount + * @return True if successful + */ + function approve(address spender, uint256 value) + external + override + whenNotPaused + notBlacklisted(msg.sender) + notBlacklisted(spender) + returns (bool) + { + _approve(msg.sender, spender, value); + return true; + } + + /** + * @dev Internal function to set allowance + * @param owner Token owner's address + * @param spender Spender's address + * @param value Allowance amount + */ + function _approve( + address owner, + address spender, + uint256 value + ) internal override { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + allowed[owner][spender] = value; + emit Approval(owner, spender, value); + } + + /** + * @notice Transfer tokens by spending allowance + * @param from Payer's address + * @param to Payee's address + * @param value Transfer amount + * @return True if successful + */ + function transferFrom( + address from, + address to, + uint256 value + ) + external + override + whenNotPaused + notBlacklisted(msg.sender) + notBlacklisted(from) + notBlacklisted(to) + returns (bool) + { + require( + value <= allowed[from][msg.sender], + "ERC20: transfer amount exceeds allowance" + ); + _transfer(from, to, value); + allowed[from][msg.sender] = allowed[from][msg.sender].sub(value); + return true; + } + + /** + * @notice Transfer tokens from the caller + * @param to Payee's address + * @param value Transfer amount + * @return True if successful + */ + function transfer(address to, uint256 value) + external + override + whenNotPaused + notBlacklisted(msg.sender) + notBlacklisted(to) + returns (bool) + { + _transfer(msg.sender, to, value); + return true; + } + + /** + * @notice Internal function to process transfers + * @param from Payer's address + * @param to Payee's address + * @param value Transfer amount + */ + function _transfer( + address from, + address to, + uint256 value + ) internal override { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + require( + value <= balances[from], + "ERC20: transfer amount exceeds balance" + ); + + balances[from] = balances[from].sub(value); + balances[to] = balances[to].add(value); + emit Transfer(from, to, value); + } + + /** + * @dev Function to add/update a new minter + * @param minter The address of the minter + * @param minterAllowedAmount The minting amount allowed for the minter + * @return True if the operation was successful. + */ + function configureMinter(address minter, uint256 minterAllowedAmount) + external + whenNotPaused + onlyMasterMinter + returns (bool) + { + minters[minter] = true; + minterAllowed[minter] = minterAllowedAmount; + emit MinterConfigured(minter, minterAllowedAmount); + return true; + } + + /** + * @dev Function to remove a minter + * @param minter The address of the minter to remove + * @return True if the operation was successful. + */ + function removeMinter(address minter) + external + onlyMasterMinter + returns (bool) + { + minters[minter] = false; + minterAllowed[minter] = 0; + emit MinterRemoved(minter); + return true; + } + + /** + * @dev allows a minter to burn some of its own tokens + * Validates that caller is a minter and that sender is not blacklisted + * amount is less than or equal to the minter's account balance + * @param _amount uint256 the amount of tokens to be burned + */ + function burn(uint256 _amount) + external + whenNotPaused + onlyMinters + notBlacklisted(msg.sender) + { + uint256 balance = balances[msg.sender]; + require(_amount > 0, "FiatToken: burn amount not greater than 0"); + require(balance >= _amount, "FiatToken: burn amount exceeds balance"); + + totalSupply_ = totalSupply_.sub(_amount); + balances[msg.sender] = balance.sub(_amount); + emit Burn(msg.sender, _amount); + emit Transfer(msg.sender, address(0), _amount); + } + + function updateMasterMinter(address _newMasterMinter) external onlyOwner { + require( + _newMasterMinter != address(0), + "FiatToken: new masterMinter is the zero address" + ); + masterMinter = _newMasterMinter; + emit MasterMinterChanged(masterMinter); + } +} + +// File: contracts/v1.1/Rescuable.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +contract Rescuable is Ownable { + using SafeERC20 for IERC20; + + address private _rescuer; + + event RescuerChanged(address indexed newRescuer); + + /** + * @notice Returns current rescuer + * @return Rescuer's address + */ + function rescuer() external view returns (address) { + return _rescuer; + } + + /** + * @notice Revert if called by any account other than the rescuer. + */ + modifier onlyRescuer() { + require(msg.sender == _rescuer, "Rescuable: caller is not the rescuer"); + _; + } + + /** + * @notice Rescue ERC20 tokens locked up in this contract. + * @param tokenContract ERC20 token contract address + * @param to Recipient address + * @param amount Amount to withdraw + */ + function rescueERC20( + IERC20 tokenContract, + address to, + uint256 amount + ) external onlyRescuer { + tokenContract.safeTransfer(to, amount); + } + + /** + * @notice Assign the rescuer role to a given address. + * @param newRescuer New rescuer's address + */ + function updateRescuer(address newRescuer) external onlyOwner { + require( + newRescuer != address(0), + "Rescuable: new rescuer is the zero address" + ); + _rescuer = newRescuer; + emit RescuerChanged(newRescuer); + } +} + +// File: contracts/v1.1/FiatTokenV1_1.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +/** + * @title FiatTokenV1_1 + * @dev ERC20 Token backed by fiat reserves + */ +contract FiatTokenV1_1 is FiatTokenV1, Rescuable { + +} + +// File: contracts/v2/AbstractFiatTokenV2.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +abstract contract AbstractFiatTokenV2 is AbstractFiatTokenV1 { + function _increaseAllowance( + address owner, + address spender, + uint256 increment + ) internal virtual; + + function _decreaseAllowance( + address owner, + address spender, + uint256 decrement + ) internal virtual; +} + +// File: contracts/util/ECRecover.sol + +/** + * Copyright (c) 2016-2019 zOS Global Limited + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +/** + * @title ECRecover + * @notice A library that provides a safe ECDSA recovery function + */ +library ECRecover { + /** + * @notice Recover signer's address from a signed message + * @dev Adapted from: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/65e4ffde586ec89af3b7e9140bdc9235d1254853/contracts/cryptography/ECDSA.sol + * Modifications: Accept v, r, and s as separate arguments + * @param digest Keccak-256 hash digest of the signed message + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + * @return Signer address + */ + function recover( + bytes32 digest, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if ( + uint256(s) > + 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 + ) { + revert("ECRecover: invalid signature 's' value"); + } + + if (v != 27 && v != 28) { + revert("ECRecover: invalid signature 'v' value"); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "ECRecover: invalid signature"); + + return signer; + } +} + +// File: contracts/util/EIP712.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +/** + * @title EIP712 + * @notice A library that provides EIP712 helper functions + */ +library EIP712 { + /** + * @notice Make EIP712 domain separator + * @param name Contract name + * @param version Contract version + * @return Domain separator + */ + function makeDomainSeparator(string memory name, string memory version) + internal + view + returns (bytes32) + { + uint256 chainId; + assembly { + chainId := chainid() + } + return + keccak256( + abi.encode( + // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, + keccak256(bytes(name)), + keccak256(bytes(version)), + chainId, + address(this) + ) + ); + } + + /** + * @notice Recover signer's address from a EIP712 signature + * @param domainSeparator Domain separator + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + * @param typeHashAndData Type hash concatenated with data + * @return Signer's address + */ + function recover( + bytes32 domainSeparator, + uint8 v, + bytes32 r, + bytes32 s, + bytes memory typeHashAndData + ) internal pure returns (address) { + bytes32 digest = + keccak256( + abi.encodePacked( + "\x19\x01", + domainSeparator, + keccak256(typeHashAndData) + ) + ); + return ECRecover.recover(digest, v, r, s); + } +} + +// File: contracts/v2/EIP712Domain.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +/** + * @title EIP712 Domain + */ +contract EIP712Domain { + /** + * @dev EIP712 Domain Separator + */ + bytes32 public DOMAIN_SEPARATOR; +} + +// File: contracts/v2/EIP3009.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +/** + * @title EIP-3009 + * @notice Provide internal implementation for gas-abstracted transfers + * @dev Contracts that inherit from this must wrap these with publicly + * accessible functions, optionally adding modifiers where necessary + */ +abstract contract EIP3009 is AbstractFiatTokenV2, EIP712Domain { + // keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)") + bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = + 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267; + + // keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)") + bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = + 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8; + + // keccak256("CancelAuthorization(address authorizer,bytes32 nonce)") + bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = + 0x158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429; + + /** + * @dev authorizer address => nonce => bool (true if nonce is used) + */ + mapping(address => mapping(bytes32 => bool)) private _authorizationStates; + + event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce); + event AuthorizationCanceled( + address indexed authorizer, + bytes32 indexed nonce + ); + + /** + * @notice Returns the state of an authorization + * @dev Nonces are randomly generated 32-byte data unique to the + * authorizer's address + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + * @return True if the nonce is used + */ + function authorizationState(address authorizer, bytes32 nonce) + external + view + returns (bool) + { + return _authorizationStates[authorizer][nonce]; + } + + /** + * @notice Execute a transfer with a signed authorization + * @param from Payer's address (Authorizer) + * @param to Payee's address + * @param value Amount to be transferred + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + * @param nonce Unique nonce + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function _transferWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + _requireValidAuthorization(from, nonce, validAfter, validBefore); + + bytes memory data = + abi.encode( + TRANSFER_WITH_AUTHORIZATION_TYPEHASH, + from, + to, + value, + validAfter, + validBefore, + nonce + ); + require( + EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == from, + "FiatTokenV2: invalid signature" + ); + + _markAuthorizationAsUsed(from, nonce); + _transfer(from, to, value); + } + + /** + * @notice Receive a transfer with a signed authorization from the payer + * @dev This has an additional check to ensure that the payee's address + * matches the caller of this function to prevent front-running attacks. + * @param from Payer's address (Authorizer) + * @param to Payee's address + * @param value Amount to be transferred + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + * @param nonce Unique nonce + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function _receiveWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + require(to == msg.sender, "FiatTokenV2: caller must be the payee"); + _requireValidAuthorization(from, nonce, validAfter, validBefore); + + bytes memory data = + abi.encode( + RECEIVE_WITH_AUTHORIZATION_TYPEHASH, + from, + to, + value, + validAfter, + validBefore, + nonce + ); + require( + EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == from, + "FiatTokenV2: invalid signature" + ); + + _markAuthorizationAsUsed(from, nonce); + _transfer(from, to, value); + } + + /** + * @notice Attempt to cancel an authorization + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function _cancelAuthorization( + address authorizer, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + _requireUnusedAuthorization(authorizer, nonce); + + bytes memory data = + abi.encode(CANCEL_AUTHORIZATION_TYPEHASH, authorizer, nonce); + require( + EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == authorizer, + "FiatTokenV2: invalid signature" + ); + + _authorizationStates[authorizer][nonce] = true; + emit AuthorizationCanceled(authorizer, nonce); + } + + /** + * @notice Check that an authorization is unused + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + */ + function _requireUnusedAuthorization(address authorizer, bytes32 nonce) + private + view + { + require( + !_authorizationStates[authorizer][nonce], + "FiatTokenV2: authorization is used or canceled" + ); + } + + /** + * @notice Check that authorization is valid + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + */ + function _requireValidAuthorization( + address authorizer, + bytes32 nonce, + uint256 validAfter, + uint256 validBefore + ) private view { + require( + now > validAfter, + "FiatTokenV2: authorization is not yet valid" + ); + require(now < validBefore, "FiatTokenV2: authorization is expired"); + _requireUnusedAuthorization(authorizer, nonce); + } + + /** + * @notice Mark an authorization as used + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + */ + function _markAuthorizationAsUsed(address authorizer, bytes32 nonce) + private + { + _authorizationStates[authorizer][nonce] = true; + emit AuthorizationUsed(authorizer, nonce); + } +} + +// File: contracts/v2/EIP2612.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +/** + * @title EIP-2612 + * @notice Provide internal implementation for gas-abstracted approvals + */ +abstract contract EIP2612 is AbstractFiatTokenV2, EIP712Domain { + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") + bytes32 public constant PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + + mapping(address => uint256) private _permitNonces; + + /** + * @notice Nonces for permit + * @param owner Token owner's address (Authorizer) + * @return Next nonce + */ + function nonces(address owner) external view returns (uint256) { + return _permitNonces[owner]; + } + + /** + * @notice Verify a signed approval permit and execute if valid + * @param owner Token owner's address (Authorizer) + * @param spender Spender's address + * @param value Amount of allowance + * @param deadline The time at which this expires (unix time) + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function _permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + require(deadline >= now, "FiatTokenV2: permit is expired"); + + bytes memory data = + abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + value, + _permitNonces[owner]++, + deadline + ); + require( + EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == owner, + "EIP2612: invalid signature" + ); + + _approve(owner, spender, value); + } +} + +// File: contracts/v2/FiatTokenV2.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +/** + * @title FiatToken V2 + * @notice ERC20 Token backed by fiat reserves, version 2 + */ +contract FiatTokenV2 is FiatTokenV1_1, EIP3009, EIP2612 { + uint8 internal _initializedVersion; + + /** + * @notice Initialize v2 + * @param newName New token name + */ + function initializeV2(string calldata newName) external { + // solhint-disable-next-line reason-string + require(initialized && _initializedVersion == 0); + name = newName; + DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(newName, "2"); + _initializedVersion = 1; + } + + /** + * @notice Increase the allowance by a given increment + * @param spender Spender's address + * @param increment Amount of increase in allowance + * @return True if successful + */ + function increaseAllowance(address spender, uint256 increment) + external + whenNotPaused + notBlacklisted(msg.sender) + notBlacklisted(spender) + returns (bool) + { + _increaseAllowance(msg.sender, spender, increment); + return true; + } + + /** + * @notice Decrease the allowance by a given decrement + * @param spender Spender's address + * @param decrement Amount of decrease in allowance + * @return True if successful + */ + function decreaseAllowance(address spender, uint256 decrement) + external + whenNotPaused + notBlacklisted(msg.sender) + notBlacklisted(spender) + returns (bool) + { + _decreaseAllowance(msg.sender, spender, decrement); + return true; + } + + /** + * @notice Execute a transfer with a signed authorization + * @param from Payer's address (Authorizer) + * @param to Payee's address + * @param value Amount to be transferred + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + * @param nonce Unique nonce + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function transferWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external whenNotPaused notBlacklisted(from) notBlacklisted(to) { + _transferWithAuthorization( + from, + to, + value, + validAfter, + validBefore, + nonce, + v, + r, + s + ); + } + + /** + * @notice Receive a transfer with a signed authorization from the payer + * @dev This has an additional check to ensure that the payee's address + * matches the caller of this function to prevent front-running attacks. + * @param from Payer's address (Authorizer) + * @param to Payee's address + * @param value Amount to be transferred + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + * @param nonce Unique nonce + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function receiveWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external whenNotPaused notBlacklisted(from) notBlacklisted(to) { + _receiveWithAuthorization( + from, + to, + value, + validAfter, + validBefore, + nonce, + v, + r, + s + ); + } + + /** + * @notice Attempt to cancel an authorization + * @dev Works only if the authorization is not yet used. + * @param authorizer Authorizer's address + * @param nonce Nonce of the authorization + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function cancelAuthorization( + address authorizer, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external whenNotPaused { + _cancelAuthorization(authorizer, nonce, v, r, s); + } + + /** + * @notice Update allowance with a signed permit + * @param owner Token owner's address (Authorizer) + * @param spender Spender's address + * @param value Amount of allowance + * @param deadline Expiration time, seconds since the epoch + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external whenNotPaused notBlacklisted(owner) notBlacklisted(spender) { + _permit(owner, spender, value, deadline, v, r, s); + } + + /** + * @notice Internal function to increase the allowance by a given increment + * @param owner Token owner's address + * @param spender Spender's address + * @param increment Amount of increase + */ + function _increaseAllowance( + address owner, + address spender, + uint256 increment + ) internal override { + _approve(owner, spender, allowed[owner][spender].add(increment)); + } + + /** + * @notice Internal function to decrease the allowance by a given decrement + * @param owner Token owner's address + * @param spender Spender's address + * @param decrement Amount of decrease + */ + function _decreaseAllowance( + address owner, + address spender, + uint256 decrement + ) internal override { + _approve( + owner, + spender, + allowed[owner][spender].sub( + decrement, + "ERC20: decreased allowance below zero" + ) + ); + } +} + +// File: contracts/v2/FiatTokenV2_1.sol + +/** + * Copyright (c) 2018-2020 CENTRE SECZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +// solhint-disable func-name-mixedcase + +/** + * @title FiatToken V2.1 + * @notice ERC20 Token backed by fiat reserves, version 2.1 + */ +contract FiatTokenV2_1 is FiatTokenV2 { + /** + * @notice Initialize v2.1 + * @param lostAndFound The address to which the locked funds are sent + */ + function initializeV2_1(address lostAndFound) external { + // solhint-disable-next-line reason-string + require(_initializedVersion == 1); + + uint256 lockedAmount = balances[address(this)]; + if (lockedAmount > 0) { + _transfer(address(this), lostAndFound, lockedAmount); + } + blacklisted[address(this)] = true; + + _initializedVersion = 2; + } + + /** + * @notice Version string for the EIP712 domain separator + * @return Version string + */ + function version() external view returns (string memory) { + return "2"; + } +} diff --git a/protocol/contracts/utils/BTCOSMedianizer.sol b/protocol/contracts/utils/BTCOSMedianizer.sol new file mode 100644 index 0000000..0660599 --- /dev/null +++ b/protocol/contracts/utils/BTCOSMedianizer.sol @@ -0,0 +1,55 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/math/SafeMath.sol"; + +import "../../interfaces/maker/OracleSecurityModule.sol"; + +interface UniswapAnchoredView { + function price(string calldata) external view returns (uint256); +} + +contract BTCOSMedianizer { + using SafeMath for uint256; + + mapping(address => bool) public authorized; + address public governance; + + OracleSecurityModule public constant OSM = OracleSecurityModule(0xf185d0682d50819263941e5f4EacC763CC5C6C42); + UniswapAnchoredView public constant MEDIANIZER = UniswapAnchoredView(0x9B8Eb8b3d6e2e0Db36F41455185FEF7049a35CaE); + string public symbol = "BTC"; + + constructor() public { + governance = msg.sender; + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setAuthorized(address _authorized) external { + require(msg.sender == governance, "!governance"); + authorized[_authorized] = true; + } + + function revokeAuthorized(address _authorized) external { + require(msg.sender == governance, "!governance"); + authorized[_authorized] = false; + } + + function read() external view returns (uint256 price, bool osm) { + if (authorized[msg.sender] && OSM.bud(address(this)) == 1) { + (bytes32 _val, bool _has) = OSM.peek(); + if (_has) return (uint256(_val), true); + } + return ((MEDIANIZER.price(symbol)).mul(1e12), false); + } + + function foresight() external view returns (uint256 price, bool osm) { + if (authorized[msg.sender] && OSM.bud(address(this)) == 1) { + (bytes32 _val, bool _has) = OSM.peep(); + if (_has) return (uint256(_val), true); + } + return ((MEDIANIZER.price(symbol)).mul(1e12), false); + } +} diff --git a/protocol/contracts/utils/CollectableDust.sol b/protocol/contracts/utils/CollectableDust.sol new file mode 100644 index 0000000..4011407 --- /dev/null +++ b/protocol/contracts/utils/CollectableDust.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.8 <0.8.0; + +import "../temp/openzeppelin/Address.sol"; +import "../temp/openzeppelin/EnumerableSet.sol"; +import "../temp/openzeppelin/IERC20.sol"; + +import "../../interfaces/utils/ICollectableDust.sol"; + +abstract contract CollectableDust is ICollectableDust { + using EnumerableSet for EnumerableSet.AddressSet; + + address + public constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + EnumerableSet.AddressSet internal protocolTokens; + + constructor() {} + + function _addProtocolToken(address _token) internal { + require( + !protocolTokens.contains(_token), + "collectable-dust::token-is-part-of-the-protocol" + ); + protocolTokens.add(_token); + } + + function _removeProtocolToken(address _token) internal { + require( + protocolTokens.contains(_token), + "collectable-dust::token-not-part-of-the-protocol" + ); + protocolTokens.remove(_token); + } + + function _sendDust( + address _to, + address _token, + uint256 _amount + ) internal { + require( + _to != address(0), + "collectable-dust::cant-send-dust-to-zero-address" + ); + require( + !protocolTokens.contains(_token), + "collectable-dust::token-is-part-of-the-protocol" + ); + if (_token == ETH_ADDRESS) { + payable(_to).transfer(_amount); + } else { + IERC20(_token).transfer(_to, _amount); + } + emit DustSent(_to, _token, _amount); + } +} diff --git a/protocol/contracts/utils/ETHOSMedianizer.sol b/protocol/contracts/utils/ETHOSMedianizer.sol new file mode 100644 index 0000000..862f5ce --- /dev/null +++ b/protocol/contracts/utils/ETHOSMedianizer.sol @@ -0,0 +1,54 @@ +pragma solidity ^0.5.17; + +import "../../interfaces/maker/OracleSecurityModule.sol"; + +interface Medianizer { + function read() external view returns (bytes32); +} + +contract ETHOSMedianizer { + mapping(address => bool) public authorized; + address public governance; + + OracleSecurityModule public constant OSM = OracleSecurityModule(0x81FE72B5A8d1A857d176C3E7d5Bd2679A9B85763); + Medianizer public constant MEDIANIZER = Medianizer(0x729D19f657BD0614b4985Cf1D82531c67569197B); + + constructor() public { + governance = msg.sender; + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setAuthorized(address _authorized) external { + require(msg.sender == governance, "!governance"); + authorized[_authorized] = true; + } + + function revokeAuthorized(address _authorized) external { + require(msg.sender == governance, "!governance"); + authorized[_authorized] = false; + } + + function read() external view returns (uint256 price, bool osm) { + if (authorized[msg.sender]) { + if (OSM.bud(address(this)) == 1) { + (bytes32 _val, ) = OSM.peek(); + return (uint256(_val), true); + } + } + return (uint256(MEDIANIZER.read()), false); + } + + function foresight() external view returns (uint256 price, bool osm) { + if (authorized[msg.sender]) { + if (OSM.bud(address(this)) == 1) { + (bytes32 _val, ) = OSM.peep(); + return (uint256(_val), true); + } + } + return (uint256(MEDIANIZER.read()), false); + } +} diff --git a/protocol/contracts/utils/Governable.sol b/protocol/contracts/utils/Governable.sol new file mode 100644 index 0000000..44bbdd7 --- /dev/null +++ b/protocol/contracts/utils/Governable.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.8; + +import "../../interfaces/utils/IGovernable.sol"; + +abstract contract Governable is IGovernable { + address public governor; + address public pendingGovernor; + + constructor(address _governor) { + require( + _governor != address(0), + "governable::governor-should-not-be-zero-address" + ); + governor = _governor; + } + + function _setPendingGovernor(address _pendingGovernor) internal { + require( + _pendingGovernor != address(0), + "governable::pending-governor-should-not-be-zero-addres" + ); + pendingGovernor = _pendingGovernor; + emit PendingGovernorSet(_pendingGovernor); + } + + function _acceptGovernor() internal { + governor = pendingGovernor; + pendingGovernor = address(0); + emit GovernorAccepted(); + } + + modifier onlyGovernor { + require(msg.sender == governor, "governable::only-governor"); + _; + } + + modifier onlyPendingGovernor { + require( + msg.sender == pendingGovernor, + "governable::only-pending-governor" + ); + _; + } +} diff --git a/protocol/contracts/vaults/LidoVault.vy b/protocol/contracts/vaults/LidoVault.vy new file mode 100644 index 0000000..8a35044 --- /dev/null +++ b/protocol/contracts/vaults/LidoVault.vy @@ -0,0 +1,190 @@ +# @version 0.2.8 +# @notice A wrapper for Lido stETH which follows Yearn Vault conventions +# @author banteg +# @license MIT +from vyper.interfaces import ERC20 + +implements: ERC20 + + +interface Lido: + def getPooledEthByShares(_sharesAmount: uint256) -> uint256: view + def getSharesByPooledEth(_pooledEthAmount: uint256) -> uint256: view + def submit(referral: address) -> uint256: payable + + +event Transfer: + sender: indexed(address) + receiver: indexed(address) + value: uint256 + + +event Approval: + owner: indexed(address) + spender: indexed(address) + value: uint256 + + +name: public(String[26]) +symbol: public(String[7]) +decimals: public(uint256) +version: public(String[1]) + +balanceOf: public(HashMap[address, uint256]) +allowance: public(HashMap[address, HashMap[address, uint256]]) +totalSupply: public(uint256) + +nonces: public(HashMap[address, uint256]) +DOMAIN_SEPARATOR: public(bytes32) +DOMAIN_TYPE_HASH: constant(bytes32) = keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') +PERMIT_TYPE_HASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") + +steth: constant(address) = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 +patron: constant(address) = 0x55Bc991b2edF3DDb4c520B222bE4F378418ff0fA + + +@external +def __init__(): + self.name = 'Yearn Lido St. Ether Vault' + self.symbol = 'yvstETH' + self.decimals = 18 + self.version = '1' + self.DOMAIN_SEPARATOR = keccak256( + concat( + DOMAIN_TYPE_HASH, + keccak256(convert(self.name, Bytes[26])), + keccak256(convert(self.version, Bytes[1])), + convert(chain.id, bytes32), + convert(self, bytes32) + ) + ) + + +@internal +def _mint(owner: address, amount: uint256): + self.totalSupply += amount + self.balanceOf[owner] += amount + log Transfer(ZERO_ADDRESS, owner, amount) + + +@internal +def _burn(owner: address, amount: uint256): + self.totalSupply -= amount + self.balanceOf[owner] -= amount + log Transfer(owner, ZERO_ADDRESS, amount) + + +@payable +@external +def __default__(): + """ + @notice Submit ether to Lido and deposit the received stETH into the Vault. + """ + shares: uint256 = Lido(steth).submit(patron, value=msg.value) + self._mint(msg.sender, shares) + + +@external +def deposit(_tokens: uint256 = MAX_UINT256, recipient: address = msg.sender) -> uint256: + """ + @notice Deposit stETH tokens into the Vault + @dev + A user must have approved the contract to spend stETH. + + @param _tokens The amount of stETH tokens to deposit + @param recipient The account to credit with the minted shares + @return The amount of minted shares + """ + tokens: uint256 = min(_tokens, ERC20(steth).balanceOf(msg.sender)) + shares: uint256 = Lido(steth).getSharesByPooledEth(tokens) + self._mint(recipient, shares) + assert ERC20(steth).transferFrom(msg.sender, self, tokens) + return shares + + +@external +def withdraw(_shares: uint256 = MAX_UINT256, recipient: address = msg.sender) -> uint256: + """ + @notice Withdraw stETH tokens from the Vault + + @param _shares The amount of shares to burn for stETH + @param recipient The account to credit with stETH + @return The amount of withdrawn stETH + """ + shares: uint256 = min(_shares, self.balanceOf[msg.sender]) + tokens: uint256 = Lido(steth).getPooledEthByShares(shares) + self._burn(msg.sender, shares) + assert ERC20(steth).transfer(recipient, tokens) + return tokens + + +@view +@external +def pricePerShare() -> uint256: + """ + @notice Get the vault share to stETH ratio + @return The value of a single share + """ + return Lido(steth).getPooledEthByShares(10 ** self.decimals) + + +@internal +def _transfer(sender: address, receiver: address, amount: uint256): + assert receiver not in [self, ZERO_ADDRESS] + self.balanceOf[sender] -= amount + self.balanceOf[receiver] += amount + log Transfer(sender, receiver, amount) + + +@external +def transfer(receiver: address, amount: uint256) -> bool: + self._transfer(msg.sender, receiver, amount) + return True + + +@external +def transferFrom(sender: address, receiver: address, amount: uint256) -> bool: + if msg.sender != sender and self.allowance[sender][msg.sender] != MAX_UINT256: + self.allowance[sender][msg.sender] -= amount + log Approval(sender, msg.sender, self.allowance[sender][msg.sender]) + self._transfer(sender, receiver, amount) + return True + + +@external +def approve(spender: address, amount: uint256) -> bool: + self.allowance[msg.sender][spender] = amount + log Approval(msg.sender, spender, amount) + return True + + +@external +def permit(owner: address, spender: address, amount: uint256, expiry: uint256, signature: Bytes[65]) -> bool: + assert owner != ZERO_ADDRESS # dev: invalid owner + assert expiry == 0 or expiry >= block.timestamp # dev: permit expired + nonce: uint256 = self.nonces[owner] + digest: bytes32 = keccak256( + concat( + b'\x19\x01', + self.DOMAIN_SEPARATOR, + keccak256( + concat( + PERMIT_TYPE_HASH, + convert(owner, bytes32), + convert(spender, bytes32), + convert(amount, bytes32), + convert(nonce, bytes32), + convert(expiry, bytes32), + ) + ) + ) + ) + # NOTE: the signature is packed as r, s, v + r: uint256 = convert(slice(signature, 0, 32), uint256) + s: uint256 = convert(slice(signature, 32, 32), uint256) + v: uint256 = convert(slice(signature, 64, 1), uint256) + assert ecrecover(digest, v, r, s) == owner # dev: invalid signature + self.allowance[owner][spender] = amount + self.nonces[owner] = nonce + 1 + log Approval(owner, spender, amount) + return True diff --git a/protocol/contracts/vaults/StakeDaoAaveUSDCVault.sol b/protocol/contracts/vaults/StakeDaoAaveUSDCVault.sol new file mode 100644 index 0000000..3bc0345 --- /dev/null +++ b/protocol/contracts/vaults/StakeDaoAaveUSDCVault.sol @@ -0,0 +1,254 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20Detailed.sol"; +import "@openzeppelinV2/contracts/ownership/Ownable.sol"; + +import "../../interfaces/yearn/IController.sol"; +import "../nft/ERC1155Tradable.sol"; +import "../nft/StakeDaoNFT.sol"; + +contract StakeDaoAaveUSDCVault is ERC20, ERC20Detailed, IERC1155TokenReceiver { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + IERC20 public token; + + uint256 public min = 9500; + uint256 public constant max = 10000; + + address public governance; + address public controller; + StakeDaoNFT public nft; + uint256 public publicReleaseTime; + + // prod + uint256 public constant rareMinId = 101; + uint256 public constant uniqueId = 111; + uint256 public constant maxAllowedId = 222; + + // test + // uint256 public constant rareMinId = 2; + // uint256 public constant uniqueId = 3; + // uint256 public constant maxAllowedId = 6; + + uint256 public constant commonLimit = 10_000 * 10**6; + uint256 public constant rareLimit = 30_000 * 10**6; + uint256 public constant uniqueLimit = 120_000 * 10**6; + + mapping(address => uint256) public lockedNFT; + + string public constant depositMessage = "Use NFT to access Strategy"; + bytes32 + public constant depositMessageDigest = 0x30ad8668dda492f609f3d154386e0644bfbe6680d092b03f6f95b78fe2b5539c; + + constructor( + address _token, + address _controller, + address _governance, + address _nft + ) + public + ERC20Detailed( + string( + abi.encodePacked("Stake DAO ", ERC20Detailed(_token).name()) + ), + string(abi.encodePacked("sd", ERC20Detailed(_token).symbol())), + ERC20Detailed(_token).decimals() + ) + { + token = IERC20(_token); + controller = _controller; + governance = _governance; + nft = StakeDaoNFT(_nft); + publicReleaseTime = block.timestamp + 30 days; + } + + function balance() public view returns (uint256) { + return + token.balanceOf(address(this)).add( + IController(controller).balanceOf(address(token)) + ); + } + + function limit(address user) public view returns (uint256) { + uint256 _nftId = lockedNFT[user]; + if (_nftId > uniqueId) _nftId -= uniqueId; + if (_nftId < rareMinId) return commonLimit; + if (_nftId < uniqueId) return rareLimit; + return uniqueLimit; + } + + function setMin(uint256 _min) external { + require(msg.sender == governance, "!governance"); + min = _min; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) public { + require(msg.sender == governance, "!governance"); + controller = _controller; + } + + function setNFT(address _nft) public { + require(msg.sender == governance, "!governance"); + nft = StakeDaoNFT(_nft); + } + + function available() public view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() public { + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + } + + function deposit( + uint256 _amount, + uint256 _nftId, + uint8 v, + bytes32 r, + bytes32 s + ) public { + require(lockedNFT[msg.sender] == 0, "NFT already locked"); + require(_nftId > 0 && _nftId <= maxAllowedId, "Invalid nft"); + + address signer = ecrecover(depositMessageDigest, v, r, s); + require(signer == msg.sender, "!signer"); + + lockedNFT[msg.sender] = _nftId; + nft.safeTransferFrom(msg.sender, address(this), _nftId, 1, ""); + _deposit(_amount); + } + + function deposit(uint256 _amount) public { + require( + block.timestamp >= publicReleaseTime || lockedNFT[msg.sender] != 0, + "NFT not locked" + ); + _deposit(_amount); + } + + function _deposit(uint256 _amount) internal { + uint256 _pool = balance(); + uint256 _totalSupply = totalSupply(); + + if (block.timestamp < publicReleaseTime) { + uint256 _existingUserAmount = _totalSupply == 0 + ? 0 + : balanceOf(msg.sender).mul(_pool).div(_totalSupply); + require( + _existingUserAmount.add(_amount) <= limit(msg.sender), + "Exceeds limit" + ); + } + + uint256 _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); + uint256 shares = 0; + if (_totalSupply == 0) { + shares = _amount; + } else { + shares = (_amount.mul(_totalSupply)).div(_pool); + } + _mint(msg.sender, shares); + } + + function harvest(address reserve, uint256 amount) external { + require(msg.sender == controller, "!controller"); + require(reserve != address(token), "token"); + IERC20(reserve).safeTransfer(controller, amount); + } + + function withdraw(uint256 _shares) public { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _withdraw = r.sub(b); + IController(controller).withdraw(address(token), _withdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + + token.safeTransfer(msg.sender, r); + + uint256 nftId = lockedNFT[msg.sender]; + if (balanceOf(msg.sender) == 0 && nftId != 0) { + lockedNFT[msg.sender] = 0; + nft.safeTransferFrom(address(this), msg.sender, nftId, 1, ""); + } + } + + function claimNFT() public { + require(block.timestamp >= publicReleaseTime, "!publicRelease"); + uint256 nftId = lockedNFT[msg.sender]; + require(nftId != 0, "!locked"); + lockedNFT[msg.sender] = 0; + nft.safeTransferFrom(address(this), msg.sender, nftId, 1, ""); + } + + function getPricePerFullShare() public view returns (uint256) { + return totalSupply() == 0 ? 1e6 : balance().mul(1e6).div(totalSupply()); + } + + function transfer(address, uint256) public returns (bool) { + revert("Restricted"); + } + + function transferFrom( + address, + address, + uint256 + ) public returns (bool) { + revert("Restricted"); + } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public returns (bytes4) { + return 0; + } + + function supportsInterface(bytes4 interfaceID) + external + view + returns (bool) + { + if (interfaceID == 0x4e2312e0) { + return true; + } + return false; + } +} diff --git a/protocol/contracts/vaults/Vault.sol b/protocol/contracts/vaults/Vault.sol new file mode 100644 index 0000000..78b0153 --- /dev/null +++ b/protocol/contracts/vaults/Vault.sol @@ -0,0 +1,146 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20Detailed.sol"; +import "@openzeppelinV2/contracts/ownership/Ownable.sol"; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} + +contract Vault is ERC20, ERC20Detailed { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + IERC20 public token; + + uint256 public min = 9500; + uint256 public constant max = 10000; + + address public governance; + address public controller; + + constructor( + address _token, + address _controller, + address _governance + ) + public + ERC20Detailed( + string( + abi.encodePacked("Stake DAO ", ERC20Detailed(_token).name()) + ), + string(abi.encodePacked("sd", ERC20Detailed(_token).symbol())), + ERC20Detailed(_token).decimals() + ) + { + token = IERC20(_token); + controller = _controller; + governance = _governance; + } + + function balance() public view returns (uint256) { + return + token.balanceOf(address(this)).add( + IController(controller).balanceOf(address(token)) + ); + } + + function setMin(uint256 _min) external { + require(msg.sender == governance, "!governance"); + min = _min; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) public { + require(msg.sender == governance, "!governance"); + controller = _controller; + } + + // Custom logic in here for how much the vault allows to be borrowed + // Sets minimum required on-hand to keep small withdrawals cheap + function available() public view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() public { + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint256 _amount) public { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + // Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + function harvest(address reserve, uint256 amount) external { + require(msg.sender == controller, "!controller"); + require(reserve != address(token), "token"); + IERC20(reserve).safeTransfer(controller, amount); + } + + // No rebalance implementation for lower fees and faster swaps + function withdraw(uint256 _shares) public { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _withdraw = r.sub(b); + IController(controller).withdraw(address(token), _withdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + + token.safeTransfer(msg.sender, r); + } + + function getPricePerFullShare() public view returns (uint256) { + return + totalSupply() == 0 ? 1e18 : balance().mul(1e18).div(totalSupply()); + } +} diff --git a/protocol/contracts/vaults/v2/Vaults.vy b/protocol/contracts/vaults/v2/Vaults.vy new file mode 100644 index 0000000..4018344 --- /dev/null +++ b/protocol/contracts/vaults/v2/Vaults.vy @@ -0,0 +1,1577 @@ +# @version 0.2.8 +""" +@title Yearn Token Vault +@license GNU AGPLv3 +@author yearn.finance +@notice + Yearn Token Vault. Holds an underlying token, and allows users to interact + with the Yearn ecosystem through Strategies connected to the Vault. + Vaults are not limited to a single Strategy, they can have as many Strategies + as can be designed (however the withdrawal queue is capped at 20.) + + Deposited funds are moved into the most impactful strategy that has not + already reached its limit for assets under management, regardless of which + Strategy a user's funds end up in, they receive their portion of yields + generated across all Strategies. + + When a user withdraws, if there are no funds sitting undeployed in the + Vault, the Vault withdraws funds from Strategies in the order of least + impact. (Funds are taken from the Strategy that will disturb everyone's + gains the least, then the next least, etc.) In order to achieve this, the + withdrawal queue's order must be properly set and managed by the community + (through governance). + + Vault Strategies are parameterized to pursue the highest risk-adjusted yield. + + There is an "Emergency Shutdown" mode. When the Vault is put into emergency + shutdown, assets will be recalled from the Strategies as quickly as is + practical (given on-chain conditions), minimizing loss. Deposits are + halted, new Strategies may not be added, and each Strategy exits with the + minimum possible damage to position, while opening up deposits to be + withdrawn by users. There are no restrictions on withdrawals above what is + expected under Normal Operation. + + For further details, please refer to the specification: + https://github.com/iearn-finance/yearn-vaults/blob/master/SPECIFICATION.md +""" + +API_VERSION: constant(String[28]) = "0.3.0" + +# TODO: Add ETH Configuration +from vyper.interfaces import ERC20 + +implements: ERC20 + + +interface DetailedERC20: + def name() -> String[42]: view + def symbol() -> String[20]: view + def decimals() -> uint256: view + + +interface Strategy: + def want() -> address: view + def vault() -> address: view + def estimatedTotalAssets() -> uint256: view + def withdraw(_amount: uint256) -> uint256: nonpayable + def migrate(_newStrategy: address): nonpayable + + +interface GuestList: + def authorized(guest: address, amount: uint256) -> bool: view + + +event Transfer: + sender: indexed(address) + receiver: indexed(address) + value: uint256 + + +event Approval: + owner: indexed(address) + spender: indexed(address) + value: uint256 + + +name: public(String[64]) +symbol: public(String[32]) +decimals: public(uint256) + +balanceOf: public(HashMap[address, uint256]) +allowance: public(HashMap[address, HashMap[address, uint256]]) +totalSupply: public(uint256) + +token: public(ERC20) +governance: public(address) +management: public(address) +guardian: public(address) +pendingGovernance: address +guestList: public(GuestList) + +struct StrategyParams: + performanceFee: uint256 # Strategist's fee (basis points) + activation: uint256 # Activation block.timestamp + debtRatio: uint256 # Maximum borrow amount (in BPS of total assets) + rateLimit: uint256 # Limit on the increase of debt per unit time since last harvest + lastReport: uint256 # block.timestamp of the last time a report occured + totalDebt: uint256 # Total outstanding debt that Strategy has + totalGain: uint256 # Total returns that Strategy has realized for Vault + totalLoss: uint256 # Total losses that Strategy has realized for Vault + + +event StrategyAdded: + strategy: indexed(address) + debtRatio: uint256 # Maximum borrow amount (in BPS of total assets) + rateLimit: uint256 # Limit on the increase of debt per unit time since last harvest + performanceFee: uint256 # Strategist's fee (basis points) + + +event StrategyReported: + strategy: indexed(address) + gain: uint256 + loss: uint256 + totalGain: uint256 + totalLoss: uint256 + totalDebt: uint256 + debtAdded: uint256 + debtRatio: uint256 + + +event UpdateGovernance: + governance: address # New active governance + + +event UpdateManagement: + management: address # New active manager + + +event UpdateGuestList: + guestList: address # Vault guest list address + + +event UpdateRewards: + rewards: address # New active rewards recipient + + +event UpdateDepositLimit: + depositLimit: uint256 # New active deposit limit + + +event UpdatePerformanceFee: + performanceFee: uint256 # New active performance fee + + +event UpdateManagementFee: + managementFee: uint256 # New active management fee + + +event UpdateGuardian: + guardian: address # Address of the active guardian + + +event EmergencyShutdown: + active: bool # New emergency shutdown state (if false, normal operation enabled) + + +event UpdateWithdrawalQueue: + queue: address[MAXIMUM_STRATEGIES] # New active withdrawal queue + + +event StrategyUpdateDebtRatio: + strategy: indexed(address) # Address of the strategy for the debt ratio adjustment + debtRatio: uint256 # The new debt limit for the strategy (in BPS of total assets) + + +event StrategyUpdateRateLimit: + strategy: indexed(address) # Address of the strategy for the rate limit adjustment + rateLimit: uint256 # The new rate limit for the strategy + + +event StrategyUpdatePerformanceFee: + strategy: indexed(address) # Address of the strategy for the performance fee adjustment + performanceFee: uint256 # The new performance fee for the strategy + + +event StrategyMigrated: + oldVersion: indexed(address) # Old version of the strategy to be migrated + newVersion: indexed(address) # New version of the strategy + + +event StrategyRevoked: + strategy: indexed(address) # Address of the strategy that is revoked + + +event StrategyRemovedFromQueue: + strategy: indexed(address) # Address of the strategy that is removed from the withdrawal queue + + +event StrategyAddedToQueue: + strategy: indexed(address) # Address of the strategy that is added to the withdrawal queue + + + +# NOTE: Track the total for overhead targeting purposes +strategies: public(HashMap[address, StrategyParams]) +MAXIMUM_STRATEGIES: constant(uint256) = 20 + +# Ordering that `withdraw` uses to determine which strategies to pull funds from +# NOTE: Does *NOT* have to match the ordering of all the current strategies that +# exist, but it is recommended that it does or else withdrawal depth is +# limited to only those inside the queue. +# NOTE: Ordering is determined by governance, and should be balanced according +# to risk, slippage, and/or volatility. Can also be ordered to increase the +# withdrawal speed of a particular Strategy. +# NOTE: The first time a ZERO_ADDRESS is encountered, it stops withdrawing +withdrawalQueue: public(address[MAXIMUM_STRATEGIES]) + +emergencyShutdown: public(bool) + +depositLimit: public(uint256) # Limit for totalAssets the Vault can hold +debtRatio: public(uint256) # Debt ratio for the Vault across all strategies (in BPS, <= 10k) +totalDebt: public(uint256) # Amount of tokens that all strategies have borrowed +lastReport: public(uint256) # block.timestamp of last report +activation: public(uint256) # block.timestamp of contract deployment + +rewards: public(address) # Rewards contract where Governance fees are sent to +# Governance Fee for management of Vault (given to `rewards`) +managementFee: public(uint256) +# Governance Fee for performance of Vault (given to `rewards`) +performanceFee: public(uint256) +MAX_BPS: constant(uint256) = 10_000 # 100%, or 10k basis points +SECS_PER_YEAR: constant(uint256) = 31_557_600 # 365.25 days +# `nonces` track `permit` approvals with signature. +nonces: public(HashMap[address, uint256]) +DOMAIN_SEPARATOR: public(bytes32) +DOMAIN_TYPE_HASH: constant(bytes32) = keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') +PERMIT_TYPE_HASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") + + +@external +def initialize( + token: address, + governance: address, + rewards: address, + nameOverride: String[64], + symbolOverride: String[32], + guardian: address = msg.sender, +): + """ + @notice + Initializes the Vault, this is called only once, when the contract is + deployed. + The performance fee is set to 10% of yield, per Strategy. + The management fee is set to 2%, per year. + The initial deposit limit is set to 0 (deposits disabled); it must be + updated after initialization. + @dev + If `nameOverride` is not specified, the name will be 'yearn' + combined with the name of `token`. + + If `symbolOverride` is not specified, the symbol will be 'y' + combined with the symbol of `token`. + @param token The token that may be deposited into this Vault. + @param governance The address authorized for governance interactions. + @param rewards The address to distribute rewards to. + @param nameOverride Specify a custom Vault name. Leave empty for default choice. + @param symbolOverride Specify a custom Vault symbol name. Leave empty for default choice. + @param guardian The address authorized for guardian interactions. Defaults to caller. + """ + assert self.activation == 0 # dev: no devops199 + self.token = ERC20(token) + if nameOverride == "": + self.name = concat(DetailedERC20(token).symbol(), " yVault") + else: + self.name = nameOverride + if symbolOverride == "": + self.symbol = concat("yv", DetailedERC20(token).symbol()) + else: + self.symbol = symbolOverride + self.decimals = DetailedERC20(token).decimals() + self.governance = governance + log UpdateGovernance(governance) + self.management = governance + log UpdateManagement(governance) + self.rewards = rewards + log UpdateRewards(rewards) + self.guardian = guardian + log UpdateGuardian(guardian) + self.performanceFee = 1000 # 10% of yield (per Strategy) + log UpdatePerformanceFee(convert(1000, uint256)) + self.managementFee = 200 # 2% per year + log UpdateManagementFee(convert(200, uint256)) + self.lastReport = block.timestamp + self.activation = block.timestamp + # EIP-712 + self.DOMAIN_SEPARATOR = keccak256( + concat( + DOMAIN_TYPE_HASH, + keccak256(convert("Yearn Vault", Bytes[11])), + keccak256(convert(API_VERSION, Bytes[28])), + convert(chain.id, bytes32), + convert(self, bytes32) + ) + ) + + +@pure +@external +def apiVersion() -> String[28]: + """ + @notice + Used to track the deployed version of this contract. In practice you + can use this version number to compare with Yearn's GitHub and + determine which version of the source matches this deployed contract. + @dev + All strategies must have an `apiVersion()` that matches the Vault's + `API_VERSION`. + @return API_VERSION which holds the current version of this contract. + """ + return API_VERSION + + +@external +def setName(name: String[42]): + """ + @notice + Used to change the value of `name`. + + This may only be called by governance. + @param name The new name to use. + """ + assert msg.sender == self.governance + self.name = name + + +@external +def setSymbol(symbol: String[20]): + """ + @notice + Used to change the value of `symbol`. + + This may only be called by governance. + @param symbol The new symbol to use. + """ + assert msg.sender == self.governance + self.symbol = symbol + + +# 2-phase commit for a change in governance +@external +def setGovernance(governance: address): + """ + @notice + Nominate a new address to use as governance. + + The change does not go into effect immediately. This function sets a + pending change, and the governance address is not updated until + the proposed governance address has accepted the responsibility. + + This may only be called by the current governance address. + @param governance The address requested to take over Vault governance. + """ + assert msg.sender == self.governance + self.pendingGovernance = governance + + +@external +def acceptGovernance(): + """ + @notice + Once a new governance address has been proposed using setGovernance(), + this function may be called by the proposed address to accept the + responsibility of taking over governance for this contract. + + This may only be called by the proposed governance address. + @dev + setGovernance() should be called by the existing governance address, + prior to calling this function. + """ + assert msg.sender == self.pendingGovernance + self.governance = msg.sender + log UpdateGovernance(msg.sender) + + +@external +def setManagement(management: address): + """ + @notice + Changes the management address. + Management is able to make some investment decisions adjusting parameters. + + This may only be called by governance. + @param management The address to use for managing. + """ + assert msg.sender == self.governance + self.management = management + log UpdateManagement(management) + + +@external +def setGuestList(guestList: address): + """ + @notice + Used to set or change `guestList`. A guest list is another contract + that dictates who is allowed to participate in a Vault (and transfer + shares). + + This may only be called by governance. + @param guestList The address of the `GuestList` contract to use. + """ + assert msg.sender == self.governance + self.guestList = GuestList(guestList) + log UpdateGuestList(guestList) + + +@external +def setRewards(rewards: address): + """ + @notice + Changes the rewards address. Any distributed rewards + will cease flowing to the old address and begin flowing + to this address once the change is in effect. + + This will not change any Strategy reports in progress, only + new reports made after this change goes into effect. + + This may only be called by governance. + @param rewards The address to use for collecting rewards. + """ + assert msg.sender == self.governance + self.rewards = rewards + log UpdateRewards(rewards) + + +@external +def setDepositLimit(limit: uint256): + """ + @notice + Changes the maximum amount of tokens that can be deposited in this Vault. + + Note, this is not how much may be deposited by a single depositor, + but the maximum amount that may be deposited across all depositors. + + This may only be called by governance. + @param limit The new deposit limit to use. + """ + assert msg.sender == self.governance + self.depositLimit = limit + log UpdateDepositLimit(limit) + + +@external +def setPerformanceFee(fee: uint256): + """ + @notice + Used to change the value of `performanceFee`. + + Should set this value below the maximum strategist performance fee. + + This may only be called by governance. + @param fee The new performance fee to use. + """ + assert msg.sender == self.governance + assert fee <= MAX_BPS + self.performanceFee = fee + log UpdatePerformanceFee(fee) + + +@external +def setManagementFee(fee: uint256): + """ + @notice + Used to change the value of `managementFee`. + + This may only be called by governance. + @param fee The new management fee to use. + """ + assert msg.sender == self.governance + assert fee <= MAX_BPS + self.managementFee = fee + log UpdateManagementFee(fee) + + +@external +def setGuardian(guardian: address): + """ + @notice + Used to change the address of `guardian`. + + This may only be called by governance or the existing guardian. + @param guardian The new guardian address to use. + """ + assert msg.sender in [self.guardian, self.governance] + self.guardian = guardian + log UpdateGuardian(guardian) + + +@external +def setEmergencyShutdown(active: bool): + """ + @notice + Activates or deactivates Vault mode where all Strategies go into full + withdrawal. + + During Emergency Shutdown: + 1. No Users may deposit into the Vault (but may withdraw as usual.) + 2. Governance may not add new Strategies. + 3. Each Strategy must pay back their debt as quickly as reasonable to + minimally affect their position. + 4. Only Governance may undo Emergency Shutdown. + + See contract level note for further details. + + This may only be called by governance or the guardian. + @param active + If true, the Vault goes into Emergency Shutdown. If false, the Vault + goes back into Normal Operation. + """ + if active: + assert msg.sender in [self.guardian, self.governance] + else: + assert msg.sender == self.governance + self.emergencyShutdown = active + log EmergencyShutdown(active) + + +@external +def setWithdrawalQueue(queue: address[MAXIMUM_STRATEGIES]): + """ + @notice + Updates the withdrawalQueue to match the addresses and order specified + by `queue`. + + There can be fewer strategies than the maximum, as well as fewer than + the total number of strategies active in the vault. `withdrawalQueue` + will be updated in a gas-efficient manner, assuming the input is well- + ordered with 0x0 only at the end. + + This may only be called by governance or management. + @dev + This is order sensitive, specify the addresses in the order in which + funds should be withdrawn (so `queue`[0] is the first Strategy withdrawn + from, `queue`[1] is the second, etc.) + + This means that the least impactful Strategy (the Strategy that will have + its core positions impacted the least by having funds removed) should be + at `queue`[0], then the next least impactful at `queue`[1], and so on. + @param queue + The array of addresses to use as the new withdrawal queue. This is + order sensitive. + """ + assert msg.sender in [self.management, self.governance] + # HACK: Temporary until Vyper adds support for Dynamic arrays + for i in range(MAXIMUM_STRATEGIES): + if queue[i] == ZERO_ADDRESS and self.withdrawalQueue[i] == ZERO_ADDRESS: + break + assert self.strategies[queue[i]].activation > 0 + self.withdrawalQueue[i] = queue[i] + log UpdateWithdrawalQueue(queue) + + +@internal +def _transfer(sender: address, receiver: address, amount: uint256): + # See note on `transfer()`. + + # Protect people from accidentally sending their shares to bad places + assert not (receiver in [self, ZERO_ADDRESS]) + self.balanceOf[sender] -= amount + self.balanceOf[receiver] += amount + log Transfer(sender, receiver, amount) + + +@external +def transfer(receiver: address, amount: uint256) -> bool: + """ + @notice + Transfers shares from the caller's address to `receiver`. This function + will always return true, unless the user is attempting to transfer + shares to this contract's address, or to 0x0. + @param receiver + The address shares are being transferred to. Must not be this contract's + address, must not be 0x0. + @param amount The quantity of shares to transfer. + @return + True if transfer is sent to an address other than this contract's or + 0x0, otherwise the transaction will fail. + """ + self._transfer(msg.sender, receiver, amount) + return True + + +@external +def transferFrom(sender: address, receiver: address, amount: uint256) -> bool: + """ + @notice + Transfers `amount` shares from `sender` to `receiver`. This operation will + always return true, unless the user is attempting to transfer shares + to this contract's address, or to 0x0. + + Unless the caller has given this contract unlimited approval, + transfering shares will decrement the caller's `allowance` by `amount`. + @param sender The address shares are being transferred from. + @param receiver + The address shares are being transferred to. Must not be this contract's + address, must not be 0x0. + @param amount The quantity of shares to transfer. + @return + True if transfer is sent to an address other than this contract's or + 0x0, otherwise the transaction will fail. + """ + # Unlimited approval (saves an SSTORE) + if (self.allowance[sender][msg.sender] < MAX_UINT256): + allowance: uint256 = self.allowance[sender][msg.sender] - amount + self.allowance[sender][msg.sender] = allowance + # NOTE: Allows log filters to have a full accounting of allowance changes + log Approval(sender, msg.sender, allowance) + self._transfer(sender, receiver, amount) + return True + + +@external +def approve(spender: address, amount: uint256) -> bool: + """ + @dev Approve the passed address to spend the specified amount of tokens on behalf of + `msg.sender`. Beware that changing an allowance with this method brings the risk + that someone may use both the old and the new allowance by unfortunate transaction + ordering. See https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param spender The address which will spend the funds. + @param amount The amount of tokens to be spent. + """ + self.allowance[msg.sender][spender] = amount + log Approval(msg.sender, spender, amount) + return True + + +@external +def increaseAllowance(spender: address, amount: uint256) -> bool: + """ + @dev Increase the allowance of the passed address to spend the total amount of tokens + on behalf of msg.sender. This method mitigates the risk that someone may use both + the old and the new allowance by unfortunate transaction ordering. + See https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param spender The address which will spend the funds. + @param amount The amount of tokens to increase the allowance by. + """ + self.allowance[msg.sender][spender] += amount + log Approval(msg.sender, spender, self.allowance[msg.sender][spender]) + return True + + +@external +def decreaseAllowance(spender: address, amount: uint256) -> bool: + """ + @dev Decrease the allowance of the passed address to spend the total amount of tokens + on behalf of msg.sender. This method mitigates the risk that someone may use both + the old and the new allowance by unfortunate transaction ordering. + See https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param spender The address which will spend the funds. + @param amount The amount of tokens to decrease the allowance by. + """ + self.allowance[msg.sender][spender] -= amount + log Approval(msg.sender, spender, self.allowance[msg.sender][spender]) + return True + + +@external +def permit(owner: address, spender: address, amount: uint256, expiry: uint256, signature: Bytes[65]) -> bool: + """ + @notice + Approves spender by owner's signature to expend owner's tokens. + See https://eips.ethereum.org/EIPS/eip-2612. + + @param owner The address which is a source of funds and has signed the Permit. + @param spender The address which is allowed to spend the funds. + @param amount The amount of tokens to be spent. + @param expiry The timestamp after which the Permit is no longer valid. + @param signature A valid secp256k1 signature of Permit by owner encoded as r, s, v. + @return True, if transaction completes successfully + """ + assert owner != ZERO_ADDRESS # dev: invalid owner + assert expiry == 0 or expiry >= block.timestamp # dev: permit expired + nonce: uint256 = self.nonces[owner] + digest: bytes32 = keccak256( + concat( + b'\x19\x01', + self.DOMAIN_SEPARATOR, + keccak256( + concat( + PERMIT_TYPE_HASH, + convert(owner, bytes32), + convert(spender, bytes32), + convert(amount, bytes32), + convert(nonce, bytes32), + convert(expiry, bytes32), + ) + ) + ) + ) + # NOTE: signature is packed as r, s, v + r: uint256 = convert(slice(signature, 0, 32), uint256) + s: uint256 = convert(slice(signature, 32, 32), uint256) + v: uint256 = convert(slice(signature, 64, 1), uint256) + assert ecrecover(digest, v, r, s) == owner # dev: invalid signature + self.allowance[owner][spender] = amount + self.nonces[owner] = nonce + 1 + log Approval(owner, spender, amount) + return True + + +@view +@internal +def _totalAssets() -> uint256: + # See note on `totalAssets()`. + return self.token.balanceOf(self) + self.totalDebt + + +@view +@external +def totalAssets() -> uint256: + """ + @notice + Returns the total quantity of all assets under control of this + Vault, whether they're loaned out to a Strategy, or currently held in + the Vault. + @return The total assets under control of this Vault. + """ + return self._totalAssets() + + +@internal +def _issueSharesForAmount(to: address, amount: uint256) -> uint256: + # Issues `amount` Vault shares to `to`. + # Shares must be issued prior to taking on new collateral, or + # calculation will be wrong. This means that only *trusted* tokens + # (with no capability for exploitative behavior) can be used. + shares: uint256 = 0 + # HACK: Saves 2 SLOADs (~4000 gas) + totalSupply: uint256 = self.totalSupply + if totalSupply > 0: + # Mint amount of shares based on what the Vault is managing overall + # NOTE: if sqrt(token.totalSupply()) > 1e39, this could potentially revert + shares = amount * totalSupply / self._totalAssets() + else: + # No existing shares, so mint 1:1 + shares = amount + + # Mint new shares + self.totalSupply = totalSupply + shares + self.balanceOf[to] += shares + log Transfer(ZERO_ADDRESS, to, shares) + + return shares + + +@external +def deposit(_amount: uint256 = MAX_UINT256, recipient: address = msg.sender) -> uint256: + """ + @notice + Deposits `_amount` `token`, issuing shares to `recipient`. If the + Vault is in Emergency Shutdown, deposits will not be accepted and this + call will fail. + @dev + Measuring quantity of shares to issues is based on the total + outstanding debt that this contract has ("expected value") instead + of the total balance sheet it has ("estimated value") has important + security considerations, and is done intentionally. If this value were + measured against external systems, it could be purposely manipulated by + an attacker to withdraw more assets than they otherwise should be able + to claim by redeeming their shares. + + On deposit, this means that shares are issued against the total amount + that the deposited capital can be given in service of the debt that + Strategies assume. If that number were to be lower than the "expected + value" at some future point, depositing shares via this method could + entitle the depositor to *less* than the deposited value once the + "realized value" is updated from further reports by the Strategies + to the Vaults. + + Care should be taken by integrators to account for this discrepancy, + by using the view-only methods of this contract (both off-chain and + on-chain) to determine if depositing into the Vault is a "good idea". + @param _amount The quantity of tokens to deposit, defaults to all. + @param recipient + The address to issue the shares in this Vault to. Defaults to the + caller's address. + @return The issued Vault shares. + """ + assert not self.emergencyShutdown # Deposits are locked out + + amount: uint256 = _amount + + # If _amount not specified, transfer the full token balance, + # up to deposit limit + if amount == MAX_UINT256: + amount = min( + self.depositLimit - self._totalAssets(), + self.token.balanceOf(msg.sender), + ) + else: + # Ensure deposit limit is respected + assert self._totalAssets() + amount <= self.depositLimit + + # Ensure we are depositing something + assert amount > 0 + + # Ensure deposit is permitted by guest list + if self.guestList.address != ZERO_ADDRESS: + assert self.guestList.authorized(msg.sender, amount) + + # Issue new shares (needs to be done before taking deposit to be accurate) + # Shares are issued to recipient (may be different from msg.sender) + # See @dev note, above. + shares: uint256 = self._issueSharesForAmount(recipient, amount) + + # Tokens are transferred from msg.sender (may be different from _recipient) + assert self.token.transferFrom(msg.sender, self, amount) + + return shares # Just in case someone wants them + + +@view +@internal +def _shareValue(shares: uint256) -> uint256: + # Determines the current value of `shares`. + # NOTE: if sqrt(Vault.totalAssets()) >>> 1e39, this could potentially revert + return (shares * (self._totalAssets())) / self.totalSupply + + +@view +@internal +def _sharesForAmount(amount: uint256) -> uint256: + # Determines how many shares `amount` of token would receive. + # See dev note on `deposit`. + if self._totalAssets() > 0: + # NOTE: if sqrt(token.totalSupply()) > 1e39, this could potentially revert + return (amount * self.totalSupply) / self._totalAssets() + else: + return 0 + + +@view +@external +def maxAvailableShares() -> uint256: + """ + @notice + Determines the total quantity of shares this Vault can provide, + factoring in assets currently residing in the Vault, as well as + those deployed to strategies. + @dev + Regarding how shares are calculated, see dev note on `deposit`. + + If you want to calculated the maximum a user could withdraw up to, + you want to use this function. + @return The total quantity of shares this Vault can provide. + """ + shares: uint256 = self._sharesForAmount(self.token.balanceOf(self)) + + for strategy in self.withdrawalQueue: + if strategy == ZERO_ADDRESS: + break + shares += self._sharesForAmount(self.strategies[strategy].totalDebt) + + return shares + + +@external +@nonreentrant("withdraw") +def withdraw( + maxShares: uint256 = MAX_UINT256, + recipient: address = msg.sender, + maxLoss: uint256 = 1, # 0.01% [BPS] +) -> uint256: + """ + @notice + Withdraws the calling account's tokens from this Vault, redeeming + amount `_shares` for an appropriate amount of tokens. + + See note on `setWithdrawalQueue` for further details of withdrawal + ordering and behavior. + @dev + Measuring the value of shares is based on the total outstanding debt + that this contract has ("expected value") instead of the total balance + sheet it has ("estimated value") has important security considerations, + and is done intentionally. If this value were measured against external + systems, it could be purposely manipulated by an attacker to withdraw + more assets than they otherwise should be able to claim by redeeming + their shares. + + On withdrawal, this means that shares are redeemed against the total + amount that the deposited capital had "realized" since the point it + was deposited, up until the point it was withdrawn. If that number + were to be higher than the "expected value" at some future point, + withdrawing shares via this method could entitle the depositor to + *more* than the expected value once the "realized value" is updated + from further reports by the Strategies to the Vaults. + + Under exceptional scenarios, this could cause earlier withdrawals to + earn "more" of the underlying assets than Users might otherwise be + entitled to, if the Vault's estimated value were otherwise measured + through external means, accounting for whatever exceptional scenarios + exist for the Vault (that aren't covered by the Vault's own design.) + @param maxShares + How many shares to try and redeem for tokens, defaults to all. + @param recipient + The address to issue the shares in this Vault to. Defaults to the + caller's address. + @param maxLoss + The maximum acceptable loss to sustain on withdrawal. Defaults to 0%. + @return The quantity of tokens redeemed for `_shares`. + """ + shares: uint256 = maxShares # May reduce this number below + + # If _shares not specified, transfer full share balance + if shares == MAX_UINT256: + shares = self.balanceOf[msg.sender] + + # Limit to only the shares they own + assert shares <= self.balanceOf[msg.sender] + + # See @dev note, above. + value: uint256 = self._shareValue(shares) + + if value > self.token.balanceOf(self): + # We need to go get some from our strategies in the withdrawal queue + # NOTE: This performs forced withdrawals from each Strategy. During + # forced withdrawal, a Strategy may realize a loss. That loss + # is reported back to the Vault, and the will affect the amount + # of tokens that the withdrawer receives for their shares. They + # can optionally specify the maximum acceptable loss (in BPS) + # to prevent excessive losses on their withdrawals (which may + # happen in certain edge cases where Strategies realize a loss) + totalLoss: uint256 = 0 + for strategy in self.withdrawalQueue: + if strategy == ZERO_ADDRESS: + break # We've exhausted the queue + + if value <= self.token.balanceOf(self): + break # We're done withdrawing + + amountNeeded: uint256 = value - self.token.balanceOf(self) + + # NOTE: Don't withdraw more than the debt so that Strategy can still + # continue to work based on the profits it has + # NOTE: This means that user will lose out on any profits that each + # Strategy in the queue would return on next harvest, benefiting others + amountNeeded = min(amountNeeded, self.strategies[strategy].totalDebt) + if amountNeeded == 0: + continue # Nothing to withdraw from this Strategy, try the next one + + # Force withdraw amount from each Strategy in the order set by governance + before: uint256 = self.token.balanceOf(self) + loss: uint256 = Strategy(strategy).withdraw(amountNeeded) + withdrawn: uint256 = self.token.balanceOf(self) - before + + # NOTE: Withdrawer incurs any losses from liquidation + if loss > 0: + value -= loss + totalLoss += loss + self.strategies[strategy].totalLoss += loss + + # Reduce the Strategy's debt by the amount withdrawn ("realized returns") + # NOTE: This doesn't add to returns as it's not earned by "normal means" + self.strategies[strategy].totalDebt -= withdrawn + loss + self.totalDebt -= withdrawn + loss + + # NOTE: This loss protection is put in place to revert if losses from + # withdrawing are more than what is considered acceptable. + assert totalLoss <= maxLoss * (value + totalLoss) / MAX_BPS + + # NOTE: We have withdrawn everything possible out of the withdrawal queue + # but we still don't have enough to fully pay them back, so adjust + # to the total amount we've freed up through forced withdrawals + if value > self.token.balanceOf(self): + value = self.token.balanceOf(self) + shares = self._sharesForAmount(value) + + # Burn shares (full value of what is being withdrawn) + self.totalSupply -= shares + self.balanceOf[msg.sender] -= shares + log Transfer(msg.sender, ZERO_ADDRESS, shares) + + # Withdraw remaining balance to _recipient (may be different to msg.sender) (minus fee) + assert self.token.transfer(recipient, value) + + return value + + +@view +@external +def pricePerShare() -> uint256: + """ + @notice Gives the price for a single Vault share. + @dev See dev note on `withdraw`. + @return The value of a single share. + """ + if self.totalSupply == 0: + return 10 ** self.decimals # price of 1:1 + else: + return self._shareValue(10 ** self.decimals) + + +@internal +def _organizeWithdrawalQueue(): + # Reorganize `withdrawalQueue` based on premise that if there is an + # empty value between two actual values, then the empty value should be + # replaced by the later value. + # NOTE: Relative ordering of non-zero values is maintained. + offset: uint256 = 0 + for idx in range(MAXIMUM_STRATEGIES): + strategy: address = self.withdrawalQueue[idx] + if strategy == ZERO_ADDRESS: + offset += 1 # how many values we need to shift, always `<= idx` + elif offset > 0: + self.withdrawalQueue[idx - offset] = strategy + self.withdrawalQueue[idx] = ZERO_ADDRESS + + +@external +def addStrategy( + strategy: address, + debtRatio: uint256, + rateLimit: uint256, + performanceFee: uint256, +): + """ + @notice + Add a Strategy to the Vault. + + This may only be called by governance. + @dev + The Strategy will be appended to `withdrawalQueue`, call + `setWithdrawalQueue` to change the order. + @param strategy The address of the Strategy to add. + @param debtRatio The ratio of the total assets in the `vault that the `strategy` can manage. + @param rateLimit + Limit on the increase of debt per unit time since last harvest + @param performanceFee + The fee the strategist will receive based on this Vault's performance. + """ + assert strategy != ZERO_ADDRESS + assert not self.emergencyShutdown + + assert msg.sender == self.governance + assert self.debtRatio + debtRatio <= MAX_BPS + assert performanceFee <= MAX_BPS - self.performanceFee + assert self.strategies[strategy].activation == 0 + assert self == Strategy(strategy).vault() + assert self.token.address == Strategy(strategy).want() + self.strategies[strategy] = StrategyParams({ + performanceFee: performanceFee, + activation: block.timestamp, + debtRatio: debtRatio, + rateLimit: rateLimit, + lastReport: block.timestamp, + totalDebt: 0, + totalGain: 0, + totalLoss: 0, + }) + self.debtRatio += debtRatio + log StrategyAdded(strategy, debtRatio, rateLimit, performanceFee) + + # queue is full + assert self.withdrawalQueue[MAXIMUM_STRATEGIES - 1] == ZERO_ADDRESS + self.withdrawalQueue[MAXIMUM_STRATEGIES - 1] = strategy + self._organizeWithdrawalQueue() + + +@external +def updateStrategyDebtRatio( + strategy: address, + debtRatio: uint256, +): + """ + @notice + Change the quantity of assets `strategy` may manage. + + This may be called by governance or management. + @param strategy The Strategy to update. + @param debtRatio The quantity of assets `strategy` may now manage. + """ + assert msg.sender in [self.management, self.governance] + assert self.strategies[strategy].activation > 0 + self.debtRatio -= self.strategies[strategy].debtRatio + self.strategies[strategy].debtRatio = debtRatio + self.debtRatio += debtRatio + assert self.debtRatio <= MAX_BPS + log StrategyUpdateDebtRatio(strategy, debtRatio) + + +@external +def updateStrategyRateLimit( + strategy: address, + rateLimit: uint256, +): + """ + @notice + Change the quantity assets per block this Vault may deposit to or + withdraw from `strategy`. + + This may only be called by governance or management. + @param strategy The Strategy to update. + @param rateLimit Limit on the increase of debt per unit time since last harvest + """ + assert msg.sender in [self.management, self.governance] + assert self.strategies[strategy].activation > 0 + self.strategies[strategy].rateLimit = rateLimit + log StrategyUpdateRateLimit(strategy, rateLimit) + + +@external +def updateStrategyPerformanceFee( + strategy: address, + performanceFee: uint256, +): + """ + @notice + Change the fee the strategist will receive based on this Vault's + performance. + + This may only be called by governance. + @param strategy The Strategy to update. + @param performanceFee The new fee the strategist will receive. + """ + assert msg.sender == self.governance + assert performanceFee <= MAX_BPS - self.performanceFee + assert self.strategies[strategy].activation > 0 + self.strategies[strategy].performanceFee = performanceFee + log StrategyUpdatePerformanceFee(strategy, performanceFee) + + +@internal +def _revokeStrategy(strategy: address): + self.debtRatio -= self.strategies[strategy].debtRatio + self.strategies[strategy].debtRatio = 0 + log StrategyRevoked(strategy) + + +@external +def migrateStrategy(oldVersion: address, newVersion: address): + """ + @notice + Migrates a Strategy, including all assets from `oldVersion` to + `newVersion`. + + This may only be called by governance. + @dev + Strategy must successfully migrate all capital and positions to new + Strategy, or else this will upset the balance of the Vault. + + The new Strategy should be "empty" e.g. have no prior commitments to + this Vault, otherwise it could have issues. + @param oldVersion The existing Strategy to migrate from. + @param newVersion The new Strategy to migrate to. + """ + assert msg.sender == self.governance + assert newVersion != ZERO_ADDRESS + assert self.strategies[oldVersion].activation > 0 + assert self.strategies[newVersion].activation == 0 + + strategy: StrategyParams = self.strategies[oldVersion] + + self._revokeStrategy(oldVersion) + # _revokeStrategy will lower the debtRatio + self.debtRatio += strategy.debtRatio + # Debt is migrated to new strategy + self.strategies[oldVersion].totalDebt = 0 + + self.strategies[newVersion] = StrategyParams({ + performanceFee: strategy.performanceFee, + activation: block.timestamp, + debtRatio: strategy.debtRatio, + rateLimit: strategy.rateLimit, + lastReport: block.timestamp, + totalDebt: strategy.totalDebt, + totalGain: 0, + totalLoss: 0, + }) + + Strategy(oldVersion).migrate(newVersion) + log StrategyMigrated(oldVersion, newVersion) + # TODO: Ensure a smooth transition in terms of Strategy return + + for idx in range(MAXIMUM_STRATEGIES): + if self.withdrawalQueue[idx] == oldVersion: + self.withdrawalQueue[idx] = newVersion + return # Don't need to reorder anything because we swapped + + +@external +def revokeStrategy(strategy: address = msg.sender): + """ + @notice + Revoke a Strategy, setting its debt limit to 0 and preventing any + future deposits. + + This function should only be used in the scenario where the Strategy is + being retired but no migration of the positions are possible, or in the + extreme scenario that the Strategy needs to be put into "Emergency Exit" + mode in order for it to exit as quickly as possible. The latter scenario + could be for any reason that is considered "critical" that the Strategy + exits its position as fast as possible, such as a sudden change in market + conditions leading to losses, or an imminent failure in an external + dependency. + + This may only be called by governance, the guardian, or the Strategy + itself. Note that a Strategy will only revoke itself during emergency + shutdown. + @param strategy The Strategy to revoke. + """ + assert msg.sender in [strategy, self.governance, self.guardian] + self._revokeStrategy(strategy) + + +@external +def addStrategyToQueue(strategy: address): + """ + @notice + Adds `strategy` to `withdrawalQueue`. + + This may only be called by governance or management. + @dev + The Strategy will be appended to `withdrawalQueue`, call + `setWithdrawalQueue` to change the order. + @param strategy The Strategy to add. + """ + assert msg.sender in [self.management, self.governance] + # Must be a current Strategy + assert self.strategies[strategy].activation > 0 + # Check if queue is full + assert self.withdrawalQueue[MAXIMUM_STRATEGIES - 1] == ZERO_ADDRESS + # Can't already be in the queue + for s in self.withdrawalQueue: + if strategy == ZERO_ADDRESS: + break + assert s != strategy + self.withdrawalQueue[MAXIMUM_STRATEGIES - 1] = strategy + self._organizeWithdrawalQueue() + log StrategyAddedToQueue(strategy) + + +@external +def removeStrategyFromQueue(strategy: address): + """ + @notice + Remove `strategy` from `withdrawalQueue`. + + This may only be called by governance or management. + @dev + We don't do this with revokeStrategy because it should still + be possible to withdraw from the Strategy if it's unwinding. + @param strategy The Strategy to remove. + """ + assert msg.sender in [self.management, self.governance] + for idx in range(MAXIMUM_STRATEGIES): + if self.withdrawalQueue[idx] == strategy: + self.withdrawalQueue[idx] = ZERO_ADDRESS + self._organizeWithdrawalQueue() + log StrategyRemovedFromQueue(strategy) + return # We found the right location and cleared it + raise # We didn't find the Strategy in the queue + + +@view +@internal +def _debtOutstanding(strategy: address) -> uint256: + # See note on `debtOutstanding()`. + strategy_debtLimit: uint256 = self.strategies[strategy].debtRatio * self._totalAssets() / MAX_BPS + strategy_totalDebt: uint256 = self.strategies[strategy].totalDebt + + if self.emergencyShutdown: + return strategy_totalDebt + elif strategy_totalDebt <= strategy_debtLimit: + return 0 + else: + return strategy_totalDebt - strategy_debtLimit + + +@view +@external +def debtOutstanding(strategy: address = msg.sender) -> uint256: + """ + @notice + Determines if `strategy` is past its debt limit and if any tokens + should be withdrawn to the Vault. + @param strategy The Strategy to check. Defaults to the caller. + @return The quantity of tokens to withdraw. + """ + return self._debtOutstanding(strategy) + + +@view +@internal +def _creditAvailable(strategy: address) -> uint256: + # See note on `creditAvailable()`. + if self.emergencyShutdown: + return 0 + + vault_totalAssets: uint256 = self._totalAssets() + vault_debtLimit: uint256 = self.debtRatio * vault_totalAssets / MAX_BPS + vault_totalDebt: uint256 = self.totalDebt + strategy_debtLimit: uint256 = self.strategies[strategy].debtRatio * vault_totalAssets / MAX_BPS + strategy_totalDebt: uint256 = self.strategies[strategy].totalDebt + strategy_rateLimit: uint256 = self.strategies[strategy].rateLimit + strategy_lastReport: uint256 = self.strategies[strategy].lastReport + + # Exhausted credit line + if strategy_debtLimit <= strategy_totalDebt or vault_debtLimit <= vault_totalDebt: + return 0 + + # Start with debt limit left for the Strategy + available: uint256 = strategy_debtLimit - strategy_totalDebt + + # Adjust by the global debt limit left + available = min(available, vault_debtLimit - vault_totalDebt) + + # Adjust by the rate limit algorithm (limits the step size per reporting period) + delta: uint256 = block.timestamp - strategy_lastReport + # NOTE: Protect against unnecessary overflow faults here + # NOTE: Set `strategy_rateLimit` to 0 to disable the rate limit + if strategy_rateLimit > 0 and available / strategy_rateLimit >= delta: + available = strategy_rateLimit * delta + + # Can only borrow up to what the contract has in reserve + # NOTE: Running near 100% is discouraged + return min(available, self.token.balanceOf(self)) + + +@view +@external +def creditAvailable(strategy: address = msg.sender) -> uint256: + """ + @notice + Amount of tokens in Vault a Strategy has access to as a credit line. + + This will check the Strategy's debt limit, as well as the tokens + available in the Vault, and determine the maximum amount of tokens + (if any) the Strategy may draw on. + + In the rare case the Vault is in emergency shutdown this will return 0. + @param strategy The Strategy to check. Defaults to caller. + @return The quantity of tokens available for the Strategy to draw on. + """ + return self._creditAvailable(strategy) + + +@view +@internal +def _expectedReturn(strategy: address) -> uint256: + # See note on `expectedReturn()`. + delta: uint256 = block.timestamp - self.strategies[strategy].lastReport + if delta > 0: + # NOTE: Unlikely to throw unless strategy accumalates >1e68 returns + # NOTE: Will not throw for DIV/0 because activation <= lastReport + return (self.strategies[strategy].totalGain * delta) / ( + block.timestamp - self.strategies[strategy].activation + ) + else: + return 0 # Covers the scenario when block.timestamp == activation + + +@view +@external +def availableDepositLimit() -> uint256: + if self.depositLimit > self._totalAssets(): + return self.depositLimit - self._totalAssets() + else: + return 0 + + +@view +@external +def expectedReturn(strategy: address = msg.sender) -> uint256: + """ + @notice + Provide an accurate expected value for the return this `strategy` + would provide to the Vault the next time `report()` is called + (since the last time it was called). + @param strategy The Strategy to determine the expected return for. Defaults to caller. + @return + The anticipated amount `strategy` should make on its investment + since its last report. + """ + return self._expectedReturn(strategy) + + +@internal +def _reportLoss(strategy: address, loss: uint256): + # Loss can only be up the amount of debt issued to strategy + totalDebt: uint256 = self.strategies[strategy].totalDebt + assert totalDebt >= loss + self.strategies[strategy].totalLoss += loss + self.strategies[strategy].totalDebt = totalDebt - loss + self.totalDebt -= loss + + # Also, make sure we reduce our trust with the strategy by the same amount + debtRatio: uint256 = self.strategies[strategy].debtRatio + self.strategies[strategy].debtRatio -= min(loss * MAX_BPS / self._totalAssets(), debtRatio) + + +@internal +def _assessFees(strategy: address, gain: uint256): + # Issue new shares to cover fees + # NOTE: In effect, this reduces overall share price by the combined fee + # NOTE: may throw if Vault.totalAssets() > 1e64, or not called for more than a year + governance_fee: uint256 = ( + (self._totalAssets() * (block.timestamp - self.lastReport) * self.managementFee) + / MAX_BPS + / SECS_PER_YEAR + ) + strategist_fee: uint256 = 0 # Only applies in certain conditions + + # NOTE: Applies if Strategy is not shutting down, or it is but all debt paid off + # NOTE: No fee is taken when a Strategy is unwinding it's position, until all debt is paid + if gain > 0: + # NOTE: Unlikely to throw unless strategy reports >1e72 harvest profit + strategist_fee = ( + gain * self.strategies[strategy].performanceFee + ) / MAX_BPS + # NOTE: Unlikely to throw unless strategy reports >1e72 harvest profit + governance_fee += gain * self.performanceFee / MAX_BPS + + # NOTE: This must be called prior to taking new collateral, + # or the calculation will be wrong! + # NOTE: This must be done at the same time, to ensure the relative + # ratio of governance_fee : strategist_fee is kept intact + total_fee: uint256 = governance_fee + strategist_fee + if total_fee > 0: # NOTE: If mgmt fee is 0% and no gains were realized, skip + reward: uint256 = self._issueSharesForAmount(self, total_fee) + + # Send the rewards out as new shares in this Vault + if strategist_fee > 0: # NOTE: Guard against DIV/0 fault + # NOTE: Unlikely to throw unless sqrt(reward) >>> 1e39 + strategist_reward: uint256 = (strategist_fee * reward) / total_fee + self._transfer(self, strategy, strategist_reward) + # NOTE: Strategy distributes rewards at the end of harvest() + # NOTE: Governance earns any dust leftover from flooring math above + if self.balanceOf[self] > 0: + self._transfer(self, self.rewards, self.balanceOf[self]) + + +@external +def report(gain: uint256, loss: uint256, _debtPayment: uint256) -> uint256: + """ + @notice + Reports the amount of assets the calling Strategy has free (usually in + terms of ROI). + + The performance fee is determined here, off of the strategy's profits + (if any), and sent to governance. + + The strategist's fee is also determined here (off of profits), to be + handled according to the strategist on the next harvest. + + This may only be called by a Strategy managed by this Vault. + @dev + For approved strategies, this is the most efficient behavior. + The Strategy reports back what it has free, then Vault "decides" + whether to take some back or give it more. Note that the most it can + take is `gain + _debtPayment`, and the most it can give is all of the + remaining reserves. Anything outside of those bounds is abnormal behavior. + + All approved strategies must have increased diligence around + calling this function, as abnormal behavior could become catastrophic. + @param gain + Amount Strategy has realized as a gain on it's investment since its + last report, and is free to be given back to Vault as earnings + @param loss + Amount Strategy has realized as a loss on it's investment since its + last report, and should be accounted for on the Vault's balance sheet + @param _debtPayment + Amount Strategy has made available to cover outstanding debt + @return Amount of debt outstanding (if totalDebt > debtLimit or emergency shutdown). + """ + + # Only approved strategies can call this function + assert self.strategies[msg.sender].activation > 0 + # No lying about total available to withdraw! + assert self.token.balanceOf(msg.sender) >= gain + _debtPayment + + # We have a loss to report, do it before the rest of the calculations + if loss > 0: + self._reportLoss(msg.sender, loss) + + # Assess both management fee and performance fee, and issue both as shares of the vault + self._assessFees(msg.sender, gain) + + # Returns are always "realized gains" + self.strategies[msg.sender].totalGain += gain + + # Outstanding debt the Strategy wants to take back from the Vault (if any) + # NOTE: debtOutstanding <= StrategyParams.totalDebt + debt: uint256 = self._debtOutstanding(msg.sender) + debtPayment: uint256 = min(_debtPayment, debt) + + if debtPayment > 0: + self.strategies[msg.sender].totalDebt -= debtPayment + self.totalDebt -= debtPayment + debt -= debtPayment + # NOTE: `debt` is being tracked for later + + # Compute the line of credit the Vault is able to offer the Strategy (if any) + credit: uint256 = self._creditAvailable(msg.sender) + + # Update the actual debt based on the full credit we are extending to the Strategy + # or the returns if we are taking funds back + # NOTE: credit + self.strategies[msg.sender].totalDebt is always < self.debtLimit + # NOTE: At least one of `credit` or `debt` is always 0 (both can be 0) + if credit > 0: + self.strategies[msg.sender].totalDebt += credit + self.totalDebt += credit + + # Give/take balance to Strategy, based on the difference between the reported gains + # (if any), the debt payment (if any), the credit increase we are offering (if any), + # and the debt needed to be paid off (if any) + # NOTE: This is just used to adjust the balance of tokens between the Strategy and + # the Vault based on the Strategy's debt limit (as well as the Vault's). + totalAvail: uint256 = gain + debtPayment + if totalAvail < credit: # credit surplus, give to Strategy + assert self.token.transfer(msg.sender, credit - totalAvail) + elif totalAvail > credit: # credit deficit, take from Strategy + assert self.token.transferFrom(msg.sender, self, totalAvail - credit) + # else, don't do anything because it is balanced + + # Update reporting time + self.strategies[msg.sender].lastReport = block.timestamp + self.lastReport = block.timestamp + + log StrategyReported( + msg.sender, + gain, + loss, + self.strategies[msg.sender].totalGain, + self.strategies[msg.sender].totalLoss, + self.strategies[msg.sender].totalDebt, + credit, + self.strategies[msg.sender].debtRatio, + ) + + if self.strategies[msg.sender].debtRatio == 0 or self.emergencyShutdown: + # Take every last penny the Strategy has (Emergency Exit/revokeStrategy) + # NOTE: This is different than `debt` in order to extract *all* of the returns + return Strategy(msg.sender).estimatedTotalAssets() + else: + # Otherwise, just return what we have as debt outstanding + return debt + + +@internal +def erc20_safe_transfer(token: address, to: address, amount: uint256): + # Used only to send tokens that are not the type managed by this Vault. + # HACK: Used to handle non-compliant tokens like USDT + response: Bytes[32] = raw_call( + token, + concat( + method_id("transfer(address,uint256)"), + convert(to, bytes32), + convert(amount, bytes32), + ), + max_outsize=32, + ) + if len(response) > 0: + assert convert(response, bool), "Transfer failed!" + + +@external +def sweep(token: address, amount: uint256 = MAX_UINT256): + """ + @notice + Removes tokens from this Vault that are not the type of token managed + by this Vault. This may be used in case of accidentally sending the + wrong kind of token to this Vault. + + Tokens will be sent to `governance`. + + This will fail if an attempt is made to sweep the tokens that this + Vault manages. + + This may only be called by governance. + @param token The token to transfer out of this vault. + @param amount The quantity or tokenId to transfer out. + """ + assert msg.sender == self.governance + # Can't be used to steal what this Vault is protecting + assert token != self.token.address + value: uint256 = amount + if value == MAX_UINT256: + value = ERC20(token).balanceOf(self) + self.erc20_safe_transfer(token, self.governance, value) \ No newline at end of file diff --git a/protocol/contracts/vaults/yDelegatedVault.sol b/protocol/contracts/vaults/yDelegatedVault.sol new file mode 100644 index 0000000..45479b8 --- /dev/null +++ b/protocol/contracts/vaults/yDelegatedVault.sol @@ -0,0 +1,344 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20Detailed.sol"; +import "@openzeppelinV2/contracts/ownership/Ownable.sol"; + +import "../../interfaces/aave/Aave.sol"; +import "../../interfaces/aave/AaveToken.sol"; +import "../../interfaces/aave/Oracle.sol"; +import "../../interfaces/aave/LendingPoolAddressesProvider.sol"; +import "../../interfaces/yearn/IController.sol"; + +contract yDelegatedVault is ERC20, ERC20Detailed { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + IERC20 public token; + + address public governance; + address public controller; + uint256 public insurance; + uint256 public healthFactor = 4; + + uint256 public ltv = 65; + uint256 public max = 100; + + address public constant aave = address( + 0x24a42fD28C976A61Df5D00D0599C34c4f90748c8 + ); + + constructor(address _token, address _controller) + public + ERC20Detailed( + string( + abi.encodePacked("stake dao ", ERC20Detailed(_token).name()) + ), + string(abi.encodePacked("sd", ERC20Detailed(_token).symbol())), + ERC20Detailed(_token).decimals() + ) + { + token = IERC20(_token); + governance = msg.sender; + controller = _controller; + } + + function debt() public view returns (uint256) { + address _reserve = IController(controller).want(address(this)); + (, uint256 currentBorrowBalance, , , , , , , , ) = Aave(getAave()) + .getUserReserveData(_reserve, address(this)); + return currentBorrowBalance; + } + + function credit() public view returns (uint256) { + return IController(controller).balanceOf(address(this)); + } + + // % of tokens locked and cannot be withdrawn per user + // this is impermanent locked, unless the debt out accrues the strategy + function locked() public view returns (uint256) { + return credit().mul(1e18).div(debt()); + } + + function debtShare(address _lp) public view returns (uint256) { + return debt().mul(balanceOf(_lp)).mul(totalSupply()); + } + + function getAave() public view returns (address) { + return LendingPoolAddressesProvider(aave).getLendingPool(); + } + + function getAaveCore() public view returns (address) { + return LendingPoolAddressesProvider(aave).getLendingPoolCore(); + } + + function setHealthFactor(uint256 _hf) external { + require(msg.sender == governance, "!governance"); + healthFactor = _hf; + } + + function activate() public { + Aave(getAave()).setUserUseReserveAsCollateral(underlying(), true); + } + + function repay(address reserve, uint256 amount) public { + // Required for certain stable coins (USDT for example) + IERC20(reserve).approve(address(getAaveCore()), 0); + IERC20(reserve).approve(address(getAaveCore()), amount); + Aave(getAave()).repay(reserve, amount, address(uint160(address(this)))); + } + + function repayAll() public { + address _reserve = reserve(); + uint256 _amount = IERC20(_reserve).balanceOf(address(this)); + repay(_reserve, _amount); + } + + // Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + function harvest(address reserve, uint256 amount) external { + require(msg.sender == controller, "!controller"); + require(reserve != address(token), "token"); + IERC20(reserve).safeTransfer(controller, amount); + } + + // Ignore insurance fund for balance calculations + function balance() public view returns (uint256) { + return token.balanceOf(address(this)).sub(insurance); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } + + function getAaveOracle() public view returns (address) { + return LendingPoolAddressesProvider(aave).getPriceOracle(); + } + + function getReservePriceETH(address reserve) public view returns (uint256) { + return Oracle(getAaveOracle()).getAssetPrice(reserve); + } + + function shouldRebalance() external view returns (bool) { + return (over() > 0); + } + + function over() public view returns (uint256) { + over(0); + } + + function getUnderlyingPriceETH(uint256 _amount) + public + view + returns (uint256) + { + _amount = _amount.mul(getUnderlyingPrice()).div( + uint256(10)**ERC20Detailed(address(token)).decimals() + ); // Calculate the amount we are withdrawing in ETH + return _amount.mul(ltv).div(max).div(healthFactor); + } + + function over(uint256 _amount) public view returns (uint256) { + address _reserve = reserve(); + uint256 _eth = getUnderlyingPriceETH(_amount); + (uint256 _maxSafeETH, uint256 _totalBorrowsETH, ) = maxSafeETH(); + _maxSafeETH = _maxSafeETH.mul(105).div(100); // 5% buffer so we don't go into a earn/rebalance loop + if (_eth > _maxSafeETH) { + _maxSafeETH = 0; + } else { + _maxSafeETH = _maxSafeETH.sub(_eth); // Add the ETH we are withdrawing + } + if (_maxSafeETH < _totalBorrowsETH) { + uint256 _over = _totalBorrowsETH + .mul(_totalBorrowsETH.sub(_maxSafeETH)) + .div(_totalBorrowsETH); + _over = _over + .mul(uint256(10)**ERC20Detailed(_reserve).decimals()) + .div(getReservePrice()); + return _over; + } else { + return 0; + } + } + + function _rebalance(uint256 _amount) internal { + uint256 _over = over(_amount); + if (_over > 0) { + if (_over > credit()) { + _over = credit(); + } + if (_over > 0) { + IController(controller).withdraw(address(this), _over); + repayAll(); + } + } + } + + function rebalance() external { + _rebalance(0); + } + + function claimInsurance() external { + require(msg.sender == controller, "!controller"); + token.safeTransfer(controller, insurance); + insurance = 0; + } + + function maxSafeETH() + public + view + returns ( + uint256 maxBorrowsETH, + uint256 totalBorrowsETH, + uint256 availableBorrowsETH + ) + { + ( + , + , + uint256 _totalBorrowsETH, + , + uint256 _availableBorrowsETH, + , + , + + ) = Aave(getAave()).getUserAccountData(address(this)); + uint256 _maxBorrowETH = (_totalBorrowsETH.add(_availableBorrowsETH)); + return ( + _maxBorrowETH.div(healthFactor), + _totalBorrowsETH, + _availableBorrowsETH + ); + } + + function shouldBorrow() external view returns (bool) { + return (availableToBorrowReserve() > 0); + } + + function availableToBorrowETH() public view returns (uint256) { + ( + uint256 _maxSafeETH, + uint256 _totalBorrowsETH, + uint256 _availableBorrowsETH + ) = maxSafeETH(); + _maxSafeETH = _maxSafeETH.mul(95).div(100); // 5% buffer so we don't go into a earn/rebalance loop + if (_maxSafeETH > _totalBorrowsETH) { + return + _availableBorrowsETH.mul(_maxSafeETH.sub(_totalBorrowsETH)).div( + _availableBorrowsETH + ); + } else { + return 0; + } + } + + function availableToBorrowReserve() public view returns (uint256) { + address _reserve = reserve(); + uint256 _available = availableToBorrowETH(); + if (_available > 0) { + return + _available + .mul(uint256(10)**ERC20Detailed(_reserve).decimals()) + .div(getReservePrice()); + } else { + return 0; + } + } + + function getReservePrice() public view returns (uint256) { + return getReservePriceETH(reserve()); + } + + function getUnderlyingPrice() public view returns (uint256) { + return getReservePriceETH(underlying()); + } + + function earn() external { + address _reserve = reserve(); + uint256 _borrow = availableToBorrowReserve(); + if (_borrow > 0) { + Aave(getAave()).borrow(_reserve, _borrow, 2, 7); + } + //rebalance here + uint256 _balance = IERC20(_reserve).balanceOf(address(this)); + if (_balance > 0) { + IERC20(_reserve).safeTransfer(controller, _balance); + IController(controller).earn(address(this), _balance); + } + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint256 _amount) public { + uint256 _pool = balance(); + token.safeTransferFrom(msg.sender, address(this), _amount); + + // 0.5% of deposits go into an insurance fund incase of negative profits to protect withdrawals + // At a 4 health factor, this is a -2% position + uint256 _insurance = _amount.mul(50).div(10000); + _amount = _amount.sub(_insurance); + insurance = insurance.add(_insurance); + + //Controller can claim insurance to liquidate to cover interest + + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + function reserve() public view returns (address) { + return IController(controller).want(address(this)); + } + + function underlying() public view returns (address) { + return AaveToken(address(token)).underlyingAssetAddress(); + } + + function withdrawAll() public { + withdraw(balanceOf(msg.sender)); + } + + // Calculates in impermanent lock due to debt + function maxWithdrawal(address account) public view returns (uint256) { + uint256 _balance = balanceOf(account); + uint256 _safeWithdraw = _balance.mul(locked()).div(1e18); + if (_safeWithdraw > _balance) { + return _balance; + } else { + uint256 _diff = _balance.sub(_safeWithdraw); + return _balance.sub(_diff.mul(healthFactor)); // technically 150%, not 200%, but adding buffer + } + } + + function safeWithdraw() external { + withdraw(maxWithdrawal(msg.sender)); + } + + // No rebalance implementation for lower fees and faster swaps + function withdraw(uint256 _shares) public { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + _rebalance(r); + token.safeTransfer(msg.sender, r); + } + + function getPricePerFullShare() external view returns (uint256) { + return balance().mul(1e18).div(totalSupply()); + } +} diff --git a/protocol/contracts/vaults/yWETH.sol b/protocol/contracts/vaults/yWETH.sol new file mode 100644 index 0000000..430fe92 --- /dev/null +++ b/protocol/contracts/vaults/yWETH.sol @@ -0,0 +1,177 @@ +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/utils/Address.sol"; +import "@openzeppelinV2/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelinV2/contracts/token/ERC20/ERC20Detailed.sol"; +import "@openzeppelinV2/contracts/ownership/Ownable.sol"; + +import "../../interfaces/weth/WETH.sol"; +import "../../interfaces/yearn/IController.sol"; + +// NOTE: The name of this contract was modified from yVault so as not to conflict with yVault.sol +contract yWETH is ERC20, ERC20Detailed { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + IERC20 public token; + + uint256 public min = 9990; + uint256 public constant max = 10000; + + address public governance; + address public controller; + + constructor(address _token, address _controller) + public + ERC20Detailed( + string( + abi.encodePacked("stake dao ", ERC20Detailed(_token).name()) + ), + string(abi.encodePacked("sd", ERC20Detailed(_token).symbol())), + ERC20Detailed(_token).decimals() + ) + { + token = IERC20(_token); + governance = msg.sender; + controller = _controller; + } + + function balance() public view returns (uint256) { + return + token.balanceOf(address(this)).add( + IController(controller).balanceOf(address(token)) + ); + } + + function setMin(uint256 _min) external { + require(msg.sender == governance, "!governance"); + min = _min; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) public { + require(msg.sender == governance, "!governance"); + controller = _controller; + } + + // Custom logic in here for how much the vault allows to be borrowed + // Sets minimum required on-hand to keep small withdrawals cheap + function available() public view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() public { + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint256 _amount) public { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + function depositETH() public payable { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + uint256 _amount = msg.value; + WETH(address(token)).deposit.value(_amount)(); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + function withdrawAllETH() external { + withdrawETH(balanceOf(msg.sender)); + } + + // Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + function harvest(address reserve, uint256 amount) external { + require(msg.sender == controller, "!controller"); + require(reserve != address(token), "token"); + IERC20(reserve).safeTransfer(controller, amount); + } + + // No rebalance implementation for lower fees and faster swaps + function withdraw(uint256 _shares) public { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + // Check balance + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _withdraw = r.sub(b); + IController(controller).withdraw(address(token), _withdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + + token.safeTransfer(msg.sender, r); + } + + // No rebalance implementation for lower fees and faster swaps + function withdrawETH(uint256 _shares) public { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + // Check balance + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _withdraw = r.sub(b); + IController(controller).withdraw(address(token), _withdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + + WETH(address(token)).withdraw(r); + address(msg.sender).transfer(r); + } + + function getPricePerFullShare() public view returns (uint256) { + return balance().mul(1e18).div(totalSupply()); + } + + function() external payable { + if (msg.sender != address(token)) { + depositETH(); + } + } +} diff --git a/protocol/interfaces/IHarvestableStrategy.sol b/protocol/interfaces/IHarvestableStrategy.sol new file mode 100644 index 0000000..8dfcfc9 --- /dev/null +++ b/protocol/interfaces/IHarvestableStrategy.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.8; + +interface IHarvestableStrategy { + function harvest() external; +} diff --git a/protocol/interfaces/IKeep3rV1.sol b/protocol/interfaces/IKeep3rV1.sol new file mode 100644 index 0000000..e317d37 --- /dev/null +++ b/protocol/interfaces/IKeep3rV1.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.8; + +interface IKeep3rV1 { + function name() external returns (string memory); + + function isKeeper(address) external returns (bool); + + function worked(address keeper) external; + + function addKPRCredit(address job, uint256 amount) external; + + function addJob(address job) external; +} diff --git a/protocol/interfaces/Keep3r/ICrvStrategyKeep3r.sol b/protocol/interfaces/Keep3r/ICrvStrategyKeep3r.sol new file mode 100644 index 0000000..f808e6e --- /dev/null +++ b/protocol/interfaces/Keep3r/ICrvStrategyKeep3r.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.8; + +interface ICrvStrategyKeep3r { + event StrategyAdded(address _strategy, uint256 _requiredHarvest); + event StrategyModified(address _strategy, uint256 _requiredHarvest); + event StrategyRemoved(address _strategy); + + function isCrvStrategyKeep3r() external pure returns (bool); + + // Setters + function addStrategy(address _strategy, uint256 _requiredHarvest) external; + + function updateRequiredHarvestAmount( + address _strategy, + uint256 _requiredHarvest + ) external; + + function removeStrategy(address _strategy) external; +} diff --git a/protocol/interfaces/Keep3r/IStrategyKeep3r.sol b/protocol/interfaces/Keep3r/IStrategyKeep3r.sol new file mode 100644 index 0000000..37342db --- /dev/null +++ b/protocol/interfaces/Keep3r/IStrategyKeep3r.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.8; + +interface IStrategyKeep3r { + event Keep3rSet(address keep3r); + // Actions by Keeper + event HarvestedByKeeper(address _strategy); + // Actions forced by governance + event HarvestedByGovernor(address _strategy); + + // Setters + function setKeep3r(address _keep3r) external; + + // Getters + function calculateHarvest(address _strategy) + external + returns (uint256 _amount); + + function workable(address _strategy) external returns (bool); + + // Keep3r actions + function harvest(address _strategy) external; + + // Governance Keeper bypass + function forceHarvest(address _strategy) external; +} diff --git a/protocol/interfaces/aave/Aave.sol b/protocol/interfaces/aave/Aave.sol new file mode 100644 index 0000000..6b1bc2a --- /dev/null +++ b/protocol/interfaces/aave/Aave.sol @@ -0,0 +1,48 @@ +pragma solidity ^0.5.17; + +interface Aave { + function borrow( + address _reserve, + uint256 _amount, + uint256 _interestRateModel, + uint16 _referralCode + ) external; + + function setUserUseReserveAsCollateral(address _reserve, bool _useAsCollateral) external; + + function repay( + address _reserve, + uint256 _amount, + address payable _onBehalfOf + ) external payable; + + function getUserAccountData(address _user) + external + view + returns ( + uint256 totalLiquidityETH, + uint256 totalCollateralETH, + uint256 totalBorrowsETH, + uint256 totalFeesETH, + uint256 availableBorrowsETH, + uint256 currentLiquidationThreshold, + uint256 ltv, + uint256 healthFactor + ); + + function getUserReserveData(address _reserve, address _user) + external + view + returns ( + uint256 currentATokenBalance, + uint256 currentBorrowBalance, + uint256 principalBorrowBalance, + uint256 borrowRateMode, + uint256 borrowRate, + uint256 liquidityRate, + uint256 originationFee, + uint256 variableBorrowIndex, + uint256 lastUpdateTimestamp, + bool usageAsCollateralEnabled + ); +} diff --git a/protocol/interfaces/aave/AaveToken.sol b/protocol/interfaces/aave/AaveToken.sol new file mode 100644 index 0000000..4daf984 --- /dev/null +++ b/protocol/interfaces/aave/AaveToken.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.5.17; + +interface AaveToken { + function underlyingAssetAddress() external view returns (address); +} diff --git a/protocol/interfaces/aave/LendingPoolAddressesProvider.sol b/protocol/interfaces/aave/LendingPoolAddressesProvider.sol new file mode 100644 index 0000000..abbf4c1 --- /dev/null +++ b/protocol/interfaces/aave/LendingPoolAddressesProvider.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.5.17; + +interface LendingPoolAddressesProvider { + function getLendingPool() external view returns (address); + + function getLendingPoolCore() external view returns (address); + + function getPriceOracle() external view returns (address); +} diff --git a/protocol/interfaces/aave/Oracle.sol b/protocol/interfaces/aave/Oracle.sol new file mode 100644 index 0000000..80c3d64 --- /dev/null +++ b/protocol/interfaces/aave/Oracle.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.5.17; + +interface Oracle { + function getAssetPrice(address reserve) external view returns (uint256); + + function latestAnswer() external view returns (uint256); +} diff --git a/protocol/interfaces/archer/ITipJar.sol b/protocol/interfaces/archer/ITipJar.sol new file mode 100644 index 0000000..4b7b93b --- /dev/null +++ b/protocol/interfaces/archer/ITipJar.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.5.17; + +interface ITipJar { + function tip() external payable; + + function updateMinerSplit( + address minerAddress, + address splitTo, + uint32 splitPct + ) external; + + function setFeeCollector(address newCollector) external; + + function setFee(uint32 newFee) external; + + function changeAdmin(address newAdmin) external; + + function upgradeTo(address newImplementation) external; + + function upgradeToAndCall(address newImplementation, bytes calldata data) + external + payable; +} diff --git a/protocol/interfaces/backscratcher/CurveGauge.json b/protocol/interfaces/backscratcher/CurveGauge.json new file mode 100644 index 0000000..ac1f531 --- /dev/null +++ b/protocol/interfaces/backscratcher/CurveGauge.json @@ -0,0 +1,256 @@ +[ + { + "name": "Deposit", + "inputs": [ + {"type": "address", "name": "provider", "indexed": true}, + {"type": "uint256", "name": "value", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "Withdraw", + "inputs": [ + {"type": "address", "name": "provider", "indexed": true}, + {"type": "uint256", "name": "value", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "UpdateLiquidityLimit", + "inputs": [ + {"type": "address", "name": "user", "indexed": false}, + {"type": "uint256", "name": "original_balance", "indexed": false}, + {"type": "uint256", "name": "original_supply", "indexed": false}, + {"type": "uint256", "name": "working_balance", "indexed": false}, + {"type": "uint256", "name": "working_supply", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "outputs": [], + "inputs": [ + {"type": "address", "name": "lp_addr"}, + {"type": "address", "name": "_minter"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "name": "user_checkpoint", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [{"type": "address", "name": "addr"}], + "stateMutability": "view", + "type": "function", + "gas": 2079152 + }, + { + "name": "claimable_tokens", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "addr"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 1998318 + }, + { + "name": "kick", + "outputs": [], + "inputs": [{"type": "address", "name": "addr"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 2084532 + }, + { + "name": "set_approve_deposit", + "outputs": [], + "inputs": [ + {"type": "address", "name": "addr"}, + {"type": "bool", "name": "can_deposit"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 35766 + }, + { + "name": "deposit", + "outputs": [], + "inputs": [{"type": "uint256", "name": "_value"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "name": "deposit", + "outputs": [], + "inputs": [ + {"type": "uint256", "name": "_value"}, + {"type": "address", "name": "addr"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "name": "withdraw", + "outputs": [], + "inputs": [{"type": "uint256", "name": "_value"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 2208318 + }, + { + "name": "integrate_checkpoint", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2297 + }, + { + "name": "minter", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1421 + }, + { + "name": "crv_token", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1451 + }, + { + "name": "lp_token", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1481 + }, + { + "name": "controller", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1511 + }, + { + "name": "voting_escrow", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1541 + }, + { + "name": "balanceOf", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1725 + }, + { + "name": "totalSupply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1601 + }, + { + "name": "future_epoch_time", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1631 + }, + { + "name": "approved_to_deposit", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "arg0"}, + {"type": "address", "name": "arg1"} + ], + "stateMutability": "view", + "type": "function", + "gas": 1969 + }, + { + "name": "working_balances", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1845 + }, + { + "name": "working_supply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1721 + }, + { + "name": "period", + "outputs": [{"type": "int128", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1751 + }, + { + "name": "period_timestamp", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "uint256", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1890 + }, + { + "name": "integrate_inv_supply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "uint256", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1920 + }, + { + "name": "integrate_inv_supply_of", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1995 + }, + { + "name": "integrate_checkpoint_of", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 2025 + }, + { + "name": "integrate_fraction", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 2055 + }, + { + "name": "inflation_rate", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1931 + } +] diff --git a/protocol/interfaces/backscratcher/CurveMinter.json b/protocol/interfaces/backscratcher/CurveMinter.json new file mode 100644 index 0000000..3800e23 --- /dev/null +++ b/protocol/interfaces/backscratcher/CurveMinter.json @@ -0,0 +1,94 @@ +[ + { + "name": "Minted", + "inputs": [ + {"type": "address", "name": "recipient", "indexed": true}, + {"type": "address", "name": "gauge", "indexed": false}, + {"type": "uint256", "name": "minted", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "outputs": [], + "inputs": [ + {"type": "address", "name": "_token"}, + {"type": "address", "name": "_controller"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "name": "mint", + "outputs": [], + "inputs": [{"type": "address", "name": "gauge_addr"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 100038 + }, + { + "name": "mint_many", + "outputs": [], + "inputs": [{"type": "address[8]", "name": "gauge_addrs"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 408502 + }, + { + "name": "mint_for", + "outputs": [], + "inputs": [ + {"type": "address", "name": "gauge_addr"}, + {"type": "address", "name": "_for"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 101219 + }, + { + "name": "toggle_approve_mint", + "outputs": [], + "inputs": [{"type": "address", "name": "minting_user"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 36726 + }, + { + "name": "token", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1301 + }, + { + "name": "controller", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1331 + }, + { + "name": "minted", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [ + {"type": "address", "name": "arg0"}, + {"type": "address", "name": "arg1"} + ], + "stateMutability": "view", + "type": "function", + "gas": 1669 + }, + { + "name": "allowed_to_mint_for", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "arg0"}, + {"type": "address", "name": "arg1"} + ], + "stateMutability": "view", + "type": "function", + "gas": 1699 + } +] diff --git a/protocol/interfaces/backscratcher/CurveRegistry.json b/protocol/interfaces/backscratcher/CurveRegistry.json new file mode 100644 index 0000000..ccdf34f --- /dev/null +++ b/protocol/interfaces/backscratcher/CurveRegistry.json @@ -0,0 +1,348 @@ +[ + { + "name": "PoolAdded", + "inputs": [ + {"type": "address", "name": "pool", "indexed": true}, + {"type": "bytes", "name": "rate_method_id", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "PoolRemoved", + "inputs": [{"type": "address", "name": "pool", "indexed": true}], + "anonymous": false, + "type": "event" + }, + { + "outputs": [], + "inputs": [ + {"type": "address", "name": "_address_provider"}, + {"type": "address", "name": "_gauge_controller"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "name": "find_pool_for_coins", + "outputs": [{"type": "address", "name": ""}], + "inputs": [ + {"type": "address", "name": "_from"}, + {"type": "address", "name": "_to"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "name": "find_pool_for_coins", + "outputs": [{"type": "address", "name": ""}], + "inputs": [ + {"type": "address", "name": "_from"}, + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "i"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "name": "get_n_coins", + "outputs": [{"type": "uint256[2]", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 1704 + }, + { + "name": "get_coins", + "outputs": [{"type": "address[8]", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 12285 + }, + { + "name": "get_underlying_coins", + "outputs": [{"type": "address[8]", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 12347 + }, + { + "name": "get_decimals", + "outputs": [{"type": "uint256[8]", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 8199 + }, + { + "name": "get_underlying_decimals", + "outputs": [{"type": "uint256[8]", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 8261 + }, + { + "name": "get_rates", + "outputs": [{"type": "uint256[8]", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 34780 + }, + { + "name": "get_gauges", + "outputs": [ + {"type": "address[10]", "name": ""}, + {"type": "int128[10]", "name": ""} + ], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 20310 + }, + { + "name": "get_balances", + "outputs": [{"type": "uint256[8]", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 16818 + }, + { + "name": "get_underlying_balances", + "outputs": [{"type": "uint256[8]", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 158953 + }, + { + "name": "get_virtual_price_from_lp_token", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "_token"}], + "stateMutability": "view", + "type": "function", + "gas": 2080 + }, + { + "name": "get_A", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 1198 + }, + { + "name": "get_parameters", + "outputs": [ + {"type": "uint256", "name": "A"}, + {"type": "uint256", "name": "future_A"}, + {"type": "uint256", "name": "fee"}, + {"type": "uint256", "name": "admin_fee"}, + {"type": "uint256", "name": "future_fee"}, + {"type": "uint256", "name": "future_admin_fee"}, + {"type": "address", "name": "future_owner"}, + {"type": "uint256", "name": "initial_A"}, + {"type": "uint256", "name": "initial_A_time"}, + {"type": "uint256", "name": "future_A_time"} + ], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 6458 + }, + { + "name": "get_fees", + "outputs": [{"type": "uint256[2]", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 1603 + }, + { + "name": "get_admin_balances", + "outputs": [{"type": "uint256[8]", "name": ""}], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "view", + "type": "function", + "gas": 36719 + }, + { + "name": "get_coin_indices", + "outputs": [ + {"type": "int128", "name": ""}, + {"type": "int128", "name": ""}, + {"type": "bool", "name": ""} + ], + "inputs": [ + {"type": "address", "name": "_pool"}, + {"type": "address", "name": "_from"}, + {"type": "address", "name": "_to"} + ], + "stateMutability": "view", + "type": "function", + "gas": 27456 + }, + { + "name": "estimate_gas_used", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [ + {"type": "address", "name": "_pool"}, + {"type": "address", "name": "_from"}, + {"type": "address", "name": "_to"} + ], + "stateMutability": "view", + "type": "function", + "gas": 32329 + }, + { + "name": "add_pool", + "outputs": [], + "inputs": [ + {"type": "address", "name": "_pool"}, + {"type": "uint256", "name": "_n_coins"}, + {"type": "address", "name": "_lp_token"}, + {"type": "bytes32", "name": "_rate_method_id"}, + {"type": "uint256", "name": "_decimals"}, + {"type": "uint256", "name": "_underlying_decimals"}, + {"type": "bool", "name": "_has_initial_A"}, + {"type": "bool", "name": "_is_v1"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 10196577 + }, + { + "name": "add_pool_without_underlying", + "outputs": [], + "inputs": [ + {"type": "address", "name": "_pool"}, + {"type": "uint256", "name": "_n_coins"}, + {"type": "address", "name": "_lp_token"}, + {"type": "bytes32", "name": "_rate_method_id"}, + {"type": "uint256", "name": "_decimals"}, + {"type": "uint256", "name": "_use_rates"}, + {"type": "bool", "name": "_has_initial_A"}, + {"type": "bool", "name": "_is_v1"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 5590664 + }, + { + "name": "add_metapool", + "outputs": [], + "inputs": [ + {"type": "address", "name": "_pool"}, + {"type": "uint256", "name": "_n_coins"}, + {"type": "address", "name": "_lp_token"}, + {"type": "uint256", "name": "_decimals"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 10226976 + }, + { + "name": "remove_pool", + "outputs": [], + "inputs": [{"type": "address", "name": "_pool"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 779646579509 + }, + { + "name": "set_pool_gas_estimates", + "outputs": [], + "inputs": [ + {"type": "address[5]", "name": "_addr"}, + {"type": "uint256[2][5]", "name": "_amount"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 355578 + }, + { + "name": "set_coin_gas_estimates", + "outputs": [], + "inputs": [ + {"type": "address[10]", "name": "_addr"}, + {"type": "uint256[10]", "name": "_amount"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 357165 + }, + { + "name": "set_gas_estimate_contract", + "outputs": [], + "inputs": [ + {"type": "address", "name": "_pool"}, + {"type": "address", "name": "_estimator"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 37747 + }, + { + "name": "set_liquidity_gauges", + "outputs": [], + "inputs": [ + {"type": "address", "name": "_pool"}, + {"type": "address[10]", "name": "_liquidity_gauges"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 365793 + }, + { + "name": "address_provider", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2111 + }, + { + "name": "gauge_controller", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2141 + }, + { + "name": "pool_list", + "outputs": [{"type": "address", "name": ""}], + "inputs": [{"type": "uint256", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 2280 + }, + { + "name": "pool_count", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 2201 + }, + { + "name": "get_pool_from_lp_token", + "outputs": [{"type": "address", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 2446 + }, + { + "name": "get_lp_token", + "outputs": [{"type": "address", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 2476 + } +] diff --git a/protocol/interfaces/backscratcher/CurveToken.json b/protocol/interfaces/backscratcher/CurveToken.json new file mode 100644 index 0000000..372e057 --- /dev/null +++ b/protocol/interfaces/backscratcher/CurveToken.json @@ -0,0 +1,268 @@ +[ + { + "name": "Transfer", + "inputs": [ + {"type": "address", "name": "_from", "indexed": true}, + {"type": "address", "name": "_to", "indexed": true}, + {"type": "uint256", "name": "_value", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "Approval", + "inputs": [ + {"type": "address", "name": "_owner", "indexed": true}, + {"type": "address", "name": "_spender", "indexed": true}, + {"type": "uint256", "name": "_value", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "UpdateMiningParameters", + "inputs": [ + {"type": "uint256", "name": "time", "indexed": false}, + {"type": "uint256", "name": "rate", "indexed": false}, + {"type": "uint256", "name": "supply", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "SetMinter", + "inputs": [{"type": "address", "name": "minter", "indexed": false}], + "anonymous": false, + "type": "event" + }, + { + "name": "SetAdmin", + "inputs": [{"type": "address", "name": "admin", "indexed": false}], + "anonymous": false, + "type": "event" + }, + { + "outputs": [], + "inputs": [ + {"type": "string", "name": "_name"}, + {"type": "string", "name": "_symbol"}, + {"type": "uint256", "name": "_decimals"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "name": "update_mining_parameters", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 148748 + }, + { + "name": "start_epoch_time_write", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 149603 + }, + { + "name": "future_epoch_time_write", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 149806 + }, + { + "name": "available_supply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 4018 + }, + { + "name": "mintable_in_timeframe", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [ + {"type": "uint256", "name": "start"}, + {"type": "uint256", "name": "end"} + ], + "stateMutability": "view", + "type": "function", + "gas": 2216141 + }, + { + "name": "set_minter", + "outputs": [], + "inputs": [{"type": "address", "name": "_minter"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 38698 + }, + { + "name": "set_admin", + "outputs": [], + "inputs": [{"type": "address", "name": "_admin"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 37837 + }, + { + "name": "totalSupply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1421 + }, + { + "name": "allowance", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [ + {"type": "address", "name": "_owner"}, + {"type": "address", "name": "_spender"} + ], + "stateMutability": "view", + "type": "function", + "gas": 1759 + }, + { + "name": "transfer", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 75139 + }, + { + "name": "transferFrom", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_from"}, + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 111433 + }, + { + "name": "approve", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_spender"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 39288 + }, + { + "name": "mint", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [ + {"type": "address", "name": "_to"}, + {"type": "uint256", "name": "_value"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 228030 + }, + { + "name": "burn", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [{"type": "uint256", "name": "_value"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 74999 + }, + { + "name": "set_name", + "outputs": [], + "inputs": [ + {"type": "string", "name": "_name"}, + {"type": "string", "name": "_symbol"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 178270 + }, + { + "name": "name", + "outputs": [{"type": "string", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 8063 + }, + { + "name": "symbol", + "outputs": [{"type": "string", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 7116 + }, + { + "name": "decimals", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1721 + }, + { + "name": "balanceOf", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1905 + }, + { + "name": "minter", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1781 + }, + { + "name": "admin", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1811 + }, + { + "name": "mining_epoch", + "outputs": [{"type": "int128", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1841 + }, + { + "name": "start_epoch_time", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1871 + }, + { + "name": "rate", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1901 + } +] diff --git a/protocol/interfaces/backscratcher/CurveVesting.json b/protocol/interfaces/backscratcher/CurveVesting.json new file mode 100644 index 0000000..b04f6d9 --- /dev/null +++ b/protocol/interfaces/backscratcher/CurveVesting.json @@ -0,0 +1,270 @@ +[ + { + "name": "Fund", + "inputs": [ + {"type": "address", "name": "recipient", "indexed": true}, + {"type": "uint256", "name": "amount", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "Claim", + "inputs": [ + {"type": "address", "name": "recipient", "indexed": true}, + {"type": "uint256", "name": "claimed", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "ToggleDisable", + "inputs": [ + {"type": "address", "name": "recipient", "indexed": false}, + {"type": "bool", "name": "disabled", "indexed": false} + ], + "anonymous": false, + "type": "event" + }, + { + "name": "CommitOwnership", + "inputs": [{"type": "address", "name": "admin", "indexed": false}], + "anonymous": false, + "type": "event" + }, + { + "name": "ApplyOwnership", + "inputs": [{"type": "address", "name": "admin", "indexed": false}], + "anonymous": false, + "type": "event" + }, + { + "outputs": [], + "inputs": [ + {"type": "address", "name": "_token"}, + {"type": "uint256", "name": "_start_time"}, + {"type": "uint256", "name": "_end_time"}, + {"type": "bool", "name": "_can_disable"}, + {"type": "address[4]", "name": "_fund_admins"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "name": "add_tokens", + "outputs": [], + "inputs": [{"type": "uint256", "name": "_amount"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 39108 + }, + { + "name": "fund", + "outputs": [], + "inputs": [ + {"type": "address[100]", "name": "_recipients"}, + {"type": "uint256[100]", "name": "_amounts"} + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": 3962646 + }, + { + "name": "toggle_disable", + "outputs": [], + "inputs": [{"type": "address", "name": "_recipient"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 40280 + }, + { + "name": "disable_can_disable", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 21295 + }, + { + "name": "disable_fund_admins", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 21325 + }, + { + "name": "vestedSupply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 4468 + }, + { + "name": "lockedSupply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 5465 + }, + { + "name": "vestedOf", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "_recipient"}], + "stateMutability": "view", + "type": "function", + "gas": 5163 + }, + { + "name": "balanceOf", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "_recipient"}], + "stateMutability": "view", + "type": "function", + "gas": 6275 + }, + { + "name": "lockedOf", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "_recipient"}], + "stateMutability": "view", + "type": "function", + "gas": 6305 + }, + { + "name": "claim", + "outputs": [], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "name": "claim", + "outputs": [], + "inputs": [{"type": "address", "name": "addr"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "name": "commit_transfer_ownership", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [{"type": "address", "name": "addr"}], + "stateMutability": "nonpayable", + "type": "function", + "gas": 38032 + }, + { + "name": "apply_transfer_ownership", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": 38932 + }, + { + "name": "token", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1601 + }, + { + "name": "start_time", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1631 + }, + { + "name": "end_time", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1661 + }, + { + "name": "initial_locked", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1845 + }, + { + "name": "total_claimed", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1875 + }, + { + "name": "initial_locked_supply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1751 + }, + { + "name": "unallocated_supply", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1781 + }, + { + "name": "can_disable", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1811 + }, + { + "name": "disabled_at", + "outputs": [{"type": "uint256", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 1995 + }, + { + "name": "admin", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1871 + }, + { + "name": "future_admin", + "outputs": [{"type": "address", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1901 + }, + { + "name": "fund_admins_enabled", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [], + "stateMutability": "view", + "type": "function", + "gas": 1931 + }, + { + "name": "fund_admins", + "outputs": [{"type": "bool", "name": ""}], + "inputs": [{"type": "address", "name": "arg0"}], + "stateMutability": "view", + "type": "function", + "gas": 2115 + } +] diff --git a/protocol/interfaces/backscratcher/ERC20.json b/protocol/interfaces/backscratcher/ERC20.json new file mode 100644 index 0000000..48f4065 --- /dev/null +++ b/protocol/interfaces/backscratcher/ERC20.json @@ -0,0 +1,231 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "name": "_name", + "type": "string" + }, + { + "name": "_symbol", + "type": "string" + }, + { + "name": "_decimals", + "type": "uint256" + }, + { + "name": "_total_supply", + "type": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "gas": 1305, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "gas": 1489, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "gas": 37793, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "gas": 76638, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "gas": 114183, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "gas": 7733, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "gas": 6786, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "gas": 1391, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "gas": 1421, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/protocol/interfaces/backscratcher/veCurveVault.json b/protocol/interfaces/backscratcher/veCurveVault.json new file mode 100644 index 0000000..6397486 --- /dev/null +++ b/protocol/interfaces/backscratcher/veCurveVault.json @@ -0,0 +1,463 @@ +[ + {"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "fromDelegate", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "toDelegate", + "type": "address" + } + ], + "name": "DelegateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "previousBalance", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" + } + ], + "name": "DelegateVotesChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "CRV", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DELEGATION_TYPEHASH", + "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DOMAINSEPARATOR", + "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DOMAIN_TYPEHASH", + "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "LOCK", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"}, + {"internalType": "address", "name": "spender", "type": "address"} + ], + "name": "allowance", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "approve", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "bal", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "uint32", "name": "", "type": "uint32"} + ], + "name": "checkpoints", + "outputs": [ + {"internalType": "uint32", "name": "fromBlock", "type": "uint32"}, + {"internalType": "uint256", "name": "votes", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "recipient", "type": "address"} + ], + "name": "claimFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "claimable", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "delegatee", "type": "address"} + ], + "name": "delegate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "delegatee", "type": "address"}, + {"internalType": "uint256", "name": "nonce", "type": "uint256"}, + {"internalType": "uint256", "name": "expiry", "type": "uint256"}, + {"internalType": "uint8", "name": "v", "type": "uint8"}, + {"internalType": "bytes32", "name": "r", "type": "bytes32"}, + {"internalType": "bytes32", "name": "s", "type": "bytes32"} + ], + "name": "delegateBySig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "delegates", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "depositAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "feeDistribution", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "getCurrentVotes", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"}, + {"internalType": "uint256", "name": "blockNumber", "type": "uint256"} + ], + "name": "getPriorVotes", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "index", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "nonces", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "numCheckpoints", + "outputs": [{"internalType": "uint32", "name": "", "type": "uint32"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingGovernance", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "owner", "type": "address"}, + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"}, + {"internalType": "uint256", "name": "deadline", "type": "uint256"}, + {"internalType": "uint8", "name": "v", "type": "uint8"}, + {"internalType": "bytes32", "name": "r", "type": "bytes32"}, + {"internalType": "bytes32", "name": "s", "type": "bytes32"} + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proxy", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewards", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "_feeDistribution", "type": "address"} + ], + "name": "setFeeDistribution", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "_governance", "type": "address"} + ], + "name": "setGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "_proxy", "type": "address"} + ], + "name": "setProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "supplyIndex", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "dst", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transfer", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "src", "type": "address"}, + {"internalType": "address", "name": "dst", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transferFrom", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "update", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "recipient", "type": "address"} + ], + "name": "updateFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/protocol/interfaces/backscratcher/yVault.json b/protocol/interfaces/backscratcher/yVault.json new file mode 100644 index 0000000..ebe65d9 --- /dev/null +++ b/protocol/interfaces/backscratcher/yVault.json @@ -0,0 +1,335 @@ +[ + { + "inputs": [ + {"internalType": "address", "name": "_token", "type": "address"}, + {"internalType": "address", "name": "_controller", "type": "address"} + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "owner", "type": "address"}, + {"internalType": "address", "name": "spender", "type": "address"} + ], + "name": "allowance", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "approve", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "available", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "balance", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "controller", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "subtractedValue", "type": "uint256"} + ], + "name": "decreaseAllowance", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_amount", "type": "uint256"} + ], + "name": "deposit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "depositAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "earn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPricePerFullShare", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "governance", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "addedValue", "type": "uint256"} + ], + "name": "increaseAllowance", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "max", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "min", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_controller", "type": "address"} + ], + "name": "setController", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "_governance", "type": "address"} + ], + "name": "setGovernance", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{"internalType": "uint256", "name": "_min", "type": "uint256"}], + "name": "setMin", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token", + "outputs": [ + {"internalType": "contract IERC20", "name": "", "type": "address"} + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "recipient", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transfer", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "sender", "type": "address"}, + {"internalType": "address", "name": "recipient", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transferFrom", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "uint256", "name": "_shares", "type": "uint256"} + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "withdrawAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/protocol/interfaces/badger/IMerkleDistributor.sol b/protocol/interfaces/badger/IMerkleDistributor.sol new file mode 100644 index 0000000..eff6090 --- /dev/null +++ b/protocol/interfaces/badger/IMerkleDistributor.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.5.0; + +// Allows anyone to claim a token if they exist in a merkle root. +interface IMerkleDistributor { + // Returns true if the index has been marked claimed. + function isClaimed(uint256 index) external view returns (bool); + + // Claim the given amount of the token to the given address. Reverts if the inputs are invalid. + function claim( + uint256 index, + address account, + uint256 amount, + bytes32[] calldata merkleProof + ) external; + + // This event is triggered whenever a call to #claim succeeds. + event Claimed(uint256 index, address account, uint256 amount); +} diff --git a/protocol/interfaces/compound/Token.sol b/protocol/interfaces/compound/Token.sol new file mode 100644 index 0000000..e3bc9c6 --- /dev/null +++ b/protocol/interfaces/compound/Token.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface cToken { + function mint(uint256 mintAmount) external returns (uint256); + + function redeem(uint256 redeemTokens) external returns (uint256); + + function redeemUnderlying(uint256 redeemAmount) external returns (uint256); + + function borrow(uint256 borrowAmount) external returns (uint256); + + function repayBorrow(uint256 repayAmount) external returns (uint256); + + function exchangeRateStored() external view returns (uint256); + + function balanceOf(address _owner) external view returns (uint256); + + function underlying() external view returns (address); +} diff --git a/protocol/interfaces/cream/Controller.sol b/protocol/interfaces/cream/Controller.sol new file mode 100644 index 0000000..1d2caab --- /dev/null +++ b/protocol/interfaces/cream/Controller.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface Creamtroller { + function claimComp(address holder) external; +} diff --git a/protocol/interfaces/crv/ICrvClaimable.sol b/protocol/interfaces/crv/ICrvClaimable.sol new file mode 100644 index 0000000..13649c1 --- /dev/null +++ b/protocol/interfaces/crv/ICrvClaimable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.8; + +interface ICrvClaimable { + function claimable_tokens(address _address) + external + returns (uint256 _amount); +} diff --git a/protocol/interfaces/crv/ICrvStrategy.sol b/protocol/interfaces/crv/ICrvStrategy.sol new file mode 100644 index 0000000..5ae1fa8 --- /dev/null +++ b/protocol/interfaces/crv/ICrvStrategy.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.8; +import "../IHarvestableStrategy.sol"; + +interface ICrvStrategy is IHarvestableStrategy { + function gauge() external pure returns (address); + + function voter() external pure returns (address); +} diff --git a/protocol/interfaces/curve/Curve.sol b/protocol/interfaces/curve/Curve.sol new file mode 100644 index 0000000..2341200 --- /dev/null +++ b/protocol/interfaces/curve/Curve.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface ICurveFi { + function get_virtual_price() external view returns (uint256); + + function add_liquidity( + // sBTC pool + uint256[3] calldata amounts, + uint256 min_mint_amount + ) external; + + function add_liquidity( + // aave pool + uint256[3] calldata amounts, + uint256 min_mint_amount, + bool use_underlying + ) external returns (uint256); + + function add_liquidity( + // bUSD pool + uint256[4] calldata amounts, + uint256 min_mint_amount + ) external; + + function add_liquidity( + // eurs pool + uint256[2] calldata amounts, + uint256 min_mint_amount + ) external; + + function remove_liquidity_imbalance( + uint256[4] calldata amounts, + uint256 max_burn_amount + ) external; + + function remove_liquidity(uint256 _amount, uint256[4] calldata amounts) + external; + + function exchange( + int128 from, + int128 to, + uint256 _from_amount, + uint256 _min_to_amount + ) external; +} + +interface Zap { + function remove_liquidity_one_coin( + uint256, + int128, + uint256 + ) external; +} diff --git a/protocol/interfaces/curve/FeeDistribution.sol b/protocol/interfaces/curve/FeeDistribution.sol new file mode 100644 index 0000000..f315965 --- /dev/null +++ b/protocol/interfaces/curve/FeeDistribution.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface FeeDistribution { + function claim(address) external returns (uint256); +} diff --git a/protocol/interfaces/curve/Gauge.sol b/protocol/interfaces/curve/Gauge.sol new file mode 100644 index 0000000..78865b9 --- /dev/null +++ b/protocol/interfaces/curve/Gauge.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface Gauge { + function deposit(uint256) external; + + function balanceOf(address) external view returns (uint256); + + function withdraw(uint256) external; + + function claim_rewards() external; +} diff --git a/protocol/interfaces/curve/Mintr.sol b/protocol/interfaces/curve/Mintr.sol new file mode 100644 index 0000000..7850526 --- /dev/null +++ b/protocol/interfaces/curve/Mintr.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface Mintr { + function mint(address) external; +} diff --git a/protocol/interfaces/curve/VoteEscrow.sol b/protocol/interfaces/curve/VoteEscrow.sol new file mode 100644 index 0000000..2ebf77e --- /dev/null +++ b/protocol/interfaces/curve/VoteEscrow.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface VoteEscrow { + function create_lock(uint256, uint256) external; + + function increase_amount(uint256) external; + + function withdraw() external; +} diff --git a/protocol/interfaces/dev/IAddressConfig.sol b/protocol/interfaces/dev/IAddressConfig.sol new file mode 100644 index 0000000..c674bca --- /dev/null +++ b/protocol/interfaces/dev/IAddressConfig.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.12; +pragma experimental ABIEncoderV2; + +interface IAddressConfig { + function lockup() external view returns (address); +} diff --git a/protocol/interfaces/dev/IDev.sol b/protocol/interfaces/dev/IDev.sol new file mode 100644 index 0000000..bf35fda --- /dev/null +++ b/protocol/interfaces/dev/IDev.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.12; +pragma experimental ABIEncoderV2; + +interface IDev { + function deposit(address _to, uint256 _amount) external returns (bool); +} diff --git a/protocol/interfaces/dev/ILockup.sol b/protocol/interfaces/dev/ILockup.sol new file mode 100644 index 0000000..048d858 --- /dev/null +++ b/protocol/interfaces/dev/ILockup.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.12; +pragma experimental ABIEncoderV2; + +interface ILockup { + function withdraw(address _property, uint256 _amount) external; + + function getValue(address _property, address _sender) + external + view + returns (uint256); + + function calculateWithdrawableInterestAmount( + address _property, + address _user + ) external view returns (uint256); +} diff --git a/protocol/interfaces/dforce/Rewards.sol b/protocol/interfaces/dforce/Rewards.sol new file mode 100644 index 0000000..1c3bfbd --- /dev/null +++ b/protocol/interfaces/dforce/Rewards.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface dRewards { + function withdraw(uint256) external; + + function getReward() external; + + function stake(uint256) external; + + function balanceOf(address) external view returns (uint256); + + function exit() external; +} diff --git a/protocol/interfaces/dforce/Token.sol b/protocol/interfaces/dforce/Token.sol new file mode 100644 index 0000000..a7a2530 --- /dev/null +++ b/protocol/interfaces/dforce/Token.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface dERC20 { + function mint(address, uint256) external; + + function redeem(address, uint256) external; + + function getTokenBalance(address) external view returns (uint256); + + function getExchangeRate() external view returns (uint256); +} diff --git a/protocol/interfaces/dydx/DydxFlashloanBase.sol b/protocol/interfaces/dydx/DydxFlashloanBase.sol new file mode 100644 index 0000000..f1d1ec1 --- /dev/null +++ b/protocol/interfaces/dydx/DydxFlashloanBase.sol @@ -0,0 +1,117 @@ +pragma solidity ^0.5.7; +pragma experimental ABIEncoderV2; + +import "@openzeppelinV2/contracts/math/SafeMath.sol"; +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; + +import "./ISoloMargin.sol"; + +contract DydxFlashloanBase { + using SafeMath for uint256; + + // -- Internal Helper functions -- // + + function _getMarketIdFromTokenAddress(address _solo, address token) + internal + view + returns (uint256) + { + ISoloMargin solo = ISoloMargin(_solo); + + uint256 numMarkets = solo.getNumMarkets(); + + address curToken; + for (uint256 i = 0; i < numMarkets; i++) { + curToken = solo.getMarketTokenAddress(i); + + if (curToken == token) { + return i; + } + } + + revert("No marketId found for provided token"); + } + + function _getRepaymentAmountInternal(uint256 amount) + internal + view + returns (uint256) + { + // Needs to be overcollateralize + // Needs to provide +2 wei to be safe + return amount.add(2); + } + + function _getAccountInfo() internal view returns (Account.Info memory) { + return Account.Info({owner: address(this), number: 1}); + } + + function _getWithdrawAction(uint marketId, uint256 amount) + internal + view + returns (Actions.ActionArgs memory) + { + return + Actions.ActionArgs({ + actionType: Actions.ActionType.Withdraw, + accountId: 0, + amount: Types.AssetAmount({ + sign: false, + denomination: Types.AssetDenomination.Wei, + ref: Types.AssetReference.Delta, + value: amount + }), + primaryMarketId: marketId, + secondaryMarketId: 0, + otherAddress: address(this), + otherAccountId: 0, + data: "" + }); + } + + function _getCallAction(bytes memory data) + internal + view + returns (Actions.ActionArgs memory) + { + return + Actions.ActionArgs({ + actionType: Actions.ActionType.Call, + accountId: 0, + amount: Types.AssetAmount({ + sign: false, + denomination: Types.AssetDenomination.Wei, + ref: Types.AssetReference.Delta, + value: 0 + }), + primaryMarketId: 0, + secondaryMarketId: 0, + otherAddress: address(this), + otherAccountId: 0, + data: data + }); + } + + function _getDepositAction(uint marketId, uint256 amount) + internal + view + returns (Actions.ActionArgs memory) + { + return + Actions.ActionArgs({ + actionType: Actions.ActionType.Deposit, + accountId: 0, + amount: Types.AssetAmount({ + sign: true, + denomination: Types.AssetDenomination.Wei, + ref: Types.AssetReference.Delta, + value: amount + }), + primaryMarketId: marketId, + secondaryMarketId: 0, + otherAddress: address(this), + otherAccountId: 0, + data: "" + }); + } +} diff --git a/protocol/interfaces/dydx/ICallee.sol b/protocol/interfaces/dydx/ICallee.sol new file mode 100644 index 0000000..2dfdab3 --- /dev/null +++ b/protocol/interfaces/dydx/ICallee.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.5.7; +pragma experimental ABIEncoderV2; + +import {Account} from "./ISoloMargin.sol"; + +/** + * @title ICallee + * @author dYdX + * + * Interface that Callees for Solo must implement in order to ingest data. + */ +contract ICallee { + // ============ Public Functions ============ + + /** + * Allows users to send this contract arbitrary data. + * + * @param sender The msg.sender to Solo + * @param accountInfo The account from which the data is being sent + * @param data Arbitrary data given by the sender + */ + function callFunction( + address sender, + Account.Info memory accountInfo, + bytes memory data + ) public; +} diff --git a/protocol/interfaces/dydx/ISoloMargin.sol b/protocol/interfaces/dydx/ISoloMargin.sol new file mode 100644 index 0000000..ee06dcf --- /dev/null +++ b/protocol/interfaces/dydx/ISoloMargin.sol @@ -0,0 +1,433 @@ +pragma solidity ^0.5.7; +pragma experimental ABIEncoderV2; + +library Account { + enum Status {Normal, Liquid, Vapor} + struct Info { + address owner; // The address that owns the account + uint256 number; // A nonce that allows a single address to control many accounts + } + struct Storage { + mapping(uint256 => Types.Par) balances; // Mapping from marketId to principal + Status status; + } +} + +library Actions { + enum ActionType { + Deposit, // supply tokens + Withdraw, // borrow tokens + Transfer, // transfer balance between accounts + Buy, // buy an amount of some token (publicly) + Sell, // sell an amount of some token (publicly) + Trade, // trade tokens against another account + Liquidate, // liquidate an undercollateralized or expiring account + Vaporize, // use excess tokens to zero-out a completely negative account + Call // send arbitrary data to an address + } + + enum AccountLayout {OnePrimary, TwoPrimary, PrimaryAndSecondary} + + enum MarketLayout {ZeroMarkets, OneMarket, TwoMarkets} + + struct ActionArgs { + ActionType actionType; + uint256 accountId; + Types.AssetAmount amount; + uint256 primaryMarketId; + uint256 secondaryMarketId; + address otherAddress; + uint256 otherAccountId; + bytes data; + } + + struct DepositArgs { + Types.AssetAmount amount; + Account.Info account; + uint256 market; + address from; + } + + struct WithdrawArgs { + Types.AssetAmount amount; + Account.Info account; + uint256 market; + address to; + } + + struct TransferArgs { + Types.AssetAmount amount; + Account.Info accountOne; + Account.Info accountTwo; + uint256 market; + } + + struct BuyArgs { + Types.AssetAmount amount; + Account.Info account; + uint256 makerMarket; + uint256 takerMarket; + address exchangeWrapper; + bytes orderData; + } + + struct SellArgs { + Types.AssetAmount amount; + Account.Info account; + uint256 takerMarket; + uint256 makerMarket; + address exchangeWrapper; + bytes orderData; + } + + struct TradeArgs { + Types.AssetAmount amount; + Account.Info takerAccount; + Account.Info makerAccount; + uint256 inputMarket; + uint256 outputMarket; + address autoTrader; + bytes tradeData; + } + + struct LiquidateArgs { + Types.AssetAmount amount; + Account.Info solidAccount; + Account.Info liquidAccount; + uint256 owedMarket; + uint256 heldMarket; + } + + struct VaporizeArgs { + Types.AssetAmount amount; + Account.Info solidAccount; + Account.Info vaporAccount; + uint256 owedMarket; + uint256 heldMarket; + } + + struct CallArgs { + Account.Info account; + address callee; + bytes data; + } +} + +library Decimal { + struct D256 { + uint256 value; + } +} + +library Interest { + struct Rate { + uint256 value; + } + + struct Index { + uint96 borrow; + uint96 supply; + uint32 lastUpdate; + } +} + +library Monetary { + struct Price { + uint256 value; + } + struct Value { + uint256 value; + } +} + +library Storage { + // All information necessary for tracking a market + struct Market { + // Contract address of the associated ERC20 token + address token; + // Total aggregated supply and borrow amount of the entire market + Types.TotalPar totalPar; + // Interest index of the market + Interest.Index index; + // Contract address of the price oracle for this market + address priceOracle; + // Contract address of the interest setter for this market + address interestSetter; + // Multiplier on the marginRatio for this market + Decimal.D256 marginPremium; + // Multiplier on the liquidationSpread for this market + Decimal.D256 spreadPremium; + // Whether additional borrows are allowed for this market + bool isClosing; + } + + // The global risk parameters that govern the health and security of the system + struct RiskParams { + // Required ratio of over-collateralization + Decimal.D256 marginRatio; + // Percentage penalty incurred by liquidated accounts + Decimal.D256 liquidationSpread; + // Percentage of the borrower's interest fee that gets passed to the suppliers + Decimal.D256 earningsRate; + // The minimum absolute borrow value of an account + // There must be sufficient incentivize to liquidate undercollateralized accounts + Monetary.Value minBorrowedValue; + } + + // The maximum RiskParam values that can be set + struct RiskLimits { + uint64 marginRatioMax; + uint64 liquidationSpreadMax; + uint64 earningsRateMax; + uint64 marginPremiumMax; + uint64 spreadPremiumMax; + uint128 minBorrowedValueMax; + } + + // The entire storage state of Solo + struct State { + // number of markets + uint256 numMarkets; + // marketId => Market + mapping(uint256 => Market) markets; + // owner => account number => Account + mapping(address => mapping(uint256 => Account.Storage)) accounts; + // Addresses that can control other users accounts + mapping(address => mapping(address => bool)) operators; + // Addresses that can control all users accounts + mapping(address => bool) globalOperators; + // mutable risk parameters of the system + RiskParams riskParams; + // immutable risk limits of the system + RiskLimits riskLimits; + } +} + +library Types { + enum AssetDenomination { + Wei, // the amount is denominated in wei + Par // the amount is denominated in par + } + + enum AssetReference { + Delta, // the amount is given as a delta from the current value + Target // the amount is given as an exact number to end up at + } + + struct AssetAmount { + bool sign; // true if positive + AssetDenomination denomination; + AssetReference ref; + uint256 value; + } + + struct TotalPar { + uint128 borrow; + uint128 supply; + } + + struct Par { + bool sign; // true if positive + uint128 value; + } + + struct Wei { + bool sign; // true if positive + uint256 value; + } +} + +contract ISoloMargin { + struct OperatorArg { + address operator; + bool trusted; + } + + function ownerSetSpreadPremium( + uint256 marketId, + Decimal.D256 memory spreadPremium + ) public; + + function getIsGlobalOperator(address operator) public view returns (bool); + + function getMarketTokenAddress(uint256 marketId) + public + view + returns (address); + + function ownerSetInterestSetter(uint256 marketId, address interestSetter) + public; + + function getAccountValues(Account.Info memory account) + public + view + returns (Monetary.Value memory, Monetary.Value memory); + + function getMarketPriceOracle(uint256 marketId) + public + view + returns (address); + + function getMarketInterestSetter(uint256 marketId) + public + view + returns (address); + + function getMarketSpreadPremium(uint256 marketId) + public + view + returns (Decimal.D256 memory); + + function getNumMarkets() public view returns (uint256); + + function ownerWithdrawUnsupportedTokens(address token, address recipient) + public + returns (uint256); + + function ownerSetMinBorrowedValue(Monetary.Value memory minBorrowedValue) + public; + + function ownerSetLiquidationSpread(Decimal.D256 memory spread) public; + + function ownerSetEarningsRate(Decimal.D256 memory earningsRate) public; + + function getIsLocalOperator(address owner, address operator) + public + view + returns (bool); + + function getAccountPar(Account.Info memory account, uint256 marketId) + public + view + returns (Types.Par memory); + + function ownerSetMarginPremium( + uint256 marketId, + Decimal.D256 memory marginPremium + ) public; + + function getMarginRatio() public view returns (Decimal.D256 memory); + + function getMarketCurrentIndex(uint256 marketId) + public + view + returns (Interest.Index memory); + + function getMarketIsClosing(uint256 marketId) public view returns (bool); + + function getRiskParams() public view returns (Storage.RiskParams memory); + + function getAccountBalances(Account.Info memory account) + public + view + returns ( + address[] memory, + Types.Par[] memory, + Types.Wei[] memory + ); + + function renounceOwnership() public; + + function getMinBorrowedValue() public view returns (Monetary.Value memory); + + function setOperators(OperatorArg[] memory args) public; + + function getMarketPrice(uint256 marketId) public view returns (address); + + function owner() public view returns (address); + + function isOwner() public view returns (bool); + + function ownerWithdrawExcessTokens(uint256 marketId, address recipient) + public + returns (uint256); + + function ownerAddMarket( + address token, + address priceOracle, + address interestSetter, + Decimal.D256 memory marginPremium, + Decimal.D256 memory spreadPremium + ) public; + + function operate( + Account.Info[] memory accounts, + Actions.ActionArgs[] memory actions + ) public; + + function getMarketWithInfo(uint256 marketId) + public + view + returns ( + Storage.Market memory, + Interest.Index memory, + Monetary.Price memory, + Interest.Rate memory + ); + + function ownerSetMarginRatio(Decimal.D256 memory ratio) public; + + function getLiquidationSpread() public view returns (Decimal.D256 memory); + + function getAccountWei(Account.Info memory account, uint256 marketId) + public + view + returns (Types.Wei memory); + + function getMarketTotalPar(uint256 marketId) + public + view + returns (Types.TotalPar memory); + + function getLiquidationSpreadForPair( + uint256 heldMarketId, + uint256 owedMarketId + ) public view returns (Decimal.D256 memory); + + function getNumExcessTokens(uint256 marketId) + public + view + returns (Types.Wei memory); + + function getMarketCachedIndex(uint256 marketId) + public + view + returns (Interest.Index memory); + + function getAccountStatus(Account.Info memory account) + public + view + returns (uint8); + + function getEarningsRate() public view returns (Decimal.D256 memory); + + function ownerSetPriceOracle(uint256 marketId, address priceOracle) public; + + function getRiskLimits() public view returns (Storage.RiskLimits memory); + + function getMarket(uint256 marketId) + public + view + returns (Storage.Market memory); + + function ownerSetIsClosing(uint256 marketId, bool isClosing) public; + + function ownerSetGlobalOperator(address operator, bool approved) public; + + function transferOwnership(address newOwner) public; + + function getAdjustedAccountValues(Account.Info memory account) + public + view + returns (Monetary.Value memory, Monetary.Value memory); + + function getMarketMarginPremium(uint256 marketId) + public + view + returns (Decimal.D256 memory); + + function getMarketInterestRate(uint256 marketId) + public + view + returns (Interest.Rate memory); +} diff --git a/protocol/interfaces/flashLoan/IERC3156FlashBorrower.sol b/protocol/interfaces/flashLoan/IERC3156FlashBorrower.sol new file mode 100644 index 0000000..ab16383 --- /dev/null +++ b/protocol/interfaces/flashLoan/IERC3156FlashBorrower.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.5.17; + +interface IERC3156FlashBorrower { + /** + * @dev Receive a flash loan. + * @param initiator The initiator of the loan. + * @param token The loan currency. + * @param amount The amount of tokens lent. + * @param fee The additional amount of tokens to repay. + * @param data Arbitrary data structure, intended to contain user-defined parameters. + * @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" + */ + function onFlashLoan( + address initiator, + address token, + uint256 amount, + uint256 fee, + bytes calldata data + ) external returns (bytes32); +} diff --git a/protocol/interfaces/flashLoan/IERC3156FlashLender.sol b/protocol/interfaces/flashLoan/IERC3156FlashLender.sol new file mode 100644 index 0000000..91de1fd --- /dev/null +++ b/protocol/interfaces/flashLoan/IERC3156FlashLender.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.5.17; +import "./IERC3156FlashBorrower.sol"; + +interface IERC3156FlashLender { + /** + * @dev The amount of currency available to be lended. + * @param token The loan currency. + * @return The amount of `token` that can be borrowed. + */ + function maxFlashLoan(address token) external view returns (uint256); + + /** + * @dev The fee to be charged for a given loan. + * @param token The loan currency. + * @param amount The amount of tokens lent. + * @return The amount of `token` to be charged for the loan, on top of the returned principal. + */ + function flashFee(address token, uint256 amount) + external + view + returns (uint256); + + /** + * @dev Initiate a flash loan. + * @param receiver The receiver of the tokens in the loan, and the receiver of the callback. + * @param token The loan currency. + * @param amount The amount of tokens lent. + * @param data Arbitrary data structure, intended to contain user-defined parameters. + */ + function flashLoan( + IERC3156FlashBorrower receiver, + address token, + uint256 amount, + bytes calldata data + ) external returns (bool); +} diff --git a/protocol/interfaces/maker/Maker.sol b/protocol/interfaces/maker/Maker.sol new file mode 100644 index 0000000..10e2907 --- /dev/null +++ b/protocol/interfaces/maker/Maker.sol @@ -0,0 +1,180 @@ +pragma solidity ^0.5.17; + +interface GemLike { + function approve(address, uint256) external; + + function transfer(address, uint256) external; + + function transferFrom( + address, + address, + uint256 + ) external; + + function deposit() external payable; + + function withdraw(uint256) external; +} + +interface ManagerLike { + function cdpCan( + address, + uint256, + address + ) external view returns (uint256); + + function ilks(uint256) external view returns (bytes32); + + function owns(uint256) external view returns (address); + + function urns(uint256) external view returns (address); + + function vat() external view returns (address); + + function open(bytes32, address) external returns (uint256); + + function give(uint256, address) external; + + function cdpAllow( + uint256, + address, + uint256 + ) external; + + function urnAllow(address, uint256) external; + + function frob( + uint256, + int256, + int256 + ) external; + + function flux( + uint256, + address, + uint256 + ) external; + + function move( + uint256, + address, + uint256 + ) external; + + function exit( + address, + uint256, + address, + uint256 + ) external; + + function quit(uint256, address) external; + + function enter(address, uint256) external; + + function shift(uint256, uint256) external; +} + +interface VatLike { + function can(address, address) external view returns (uint256); + + function ilks(bytes32) + external + view + returns ( + uint256, + uint256, + uint256, + uint256, + uint256 + ); + + function dai(address) external view returns (uint256); + + function urns(bytes32, address) external view returns (uint256, uint256); + + function frob( + bytes32, + address, + address, + address, + int256, + int256 + ) external; + + function hope(address) external; + + function move( + address, + address, + uint256 + ) external; +} + +interface GemJoinLike { + function dec() external returns (uint256); + + function gem() external returns (GemLike); + + function join(address, uint256) external payable; + + function exit(address, uint256) external; +} + +interface GNTJoinLike { + function bags(address) external view returns (address); + + function make(address) external returns (address); +} + +interface DaiJoinLike { + function vat() external returns (VatLike); + + function dai() external returns (GemLike); + + function join(address, uint256) external payable; + + function exit(address, uint256) external; +} + +interface HopeLike { + function hope(address) external; + + function nope(address) external; +} + +interface EndLike { + function fix(bytes32) external view returns (uint256); + + function cash(bytes32, uint256) external; + + function free(bytes32) external; + + function pack(uint256) external; + + function skim(bytes32, address) external; +} + +interface JugLike { + function drip(bytes32) external returns (uint256); +} + +interface PotLike { + function pie(address) external view returns (uint256); + + function drip() external returns (uint256); + + function join(uint256) external; + + function exit(uint256) external; +} + +interface SpotLike { + function ilks(bytes32) external view returns (address, uint256); +} + +interface OSMedianizer { + function read() external view returns (uint256, bool); + + function foresight() external view returns (uint256, bool); +} diff --git a/protocol/interfaces/maker/OracleSecurityModule.sol b/protocol/interfaces/maker/OracleSecurityModule.sol new file mode 100644 index 0000000..e8b6c2c --- /dev/null +++ b/protocol/interfaces/maker/OracleSecurityModule.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.5.17; + +interface OracleSecurityModule { + function peek() external view returns (bytes32, bool); + + function peep() external view returns (bytes32, bool); + + function bud(address) external view returns (uint256); +} diff --git a/protocol/interfaces/uniswap/Uni.sol b/protocol/interfaces/uniswap/Uni.sol new file mode 100644 index 0000000..5bf89b6 --- /dev/null +++ b/protocol/interfaces/uniswap/Uni.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface Uni { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; + + function swapExactETHForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + function swapExactTokensForETH( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); +} diff --git a/protocol/interfaces/utils/ICollectableDust.sol b/protocol/interfaces/utils/ICollectableDust.sol new file mode 100644 index 0000000..ef7ee7f --- /dev/null +++ b/protocol/interfaces/utils/ICollectableDust.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.8; + +interface ICollectableDust { + event DustSent(address _to, address token, uint256 amount); + + function sendDust( + address _to, + address _token, + uint256 _amount + ) external; +} diff --git a/protocol/interfaces/utils/IGovernable.sol b/protocol/interfaces/utils/IGovernable.sol new file mode 100644 index 0000000..feeaee2 --- /dev/null +++ b/protocol/interfaces/utils/IGovernable.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.8; + +interface IGovernable { + event PendingGovernorSet(address pendingGovernor); + event GovernorAccepted(); + + function setPendingGovernor(address _pendingGovernor) external; + + function acceptGovernor() external; +} diff --git a/protocol/interfaces/weth/WETH.sol b/protocol/interfaces/weth/WETH.sol new file mode 100644 index 0000000..4479cc9 --- /dev/null +++ b/protocol/interfaces/weth/WETH.sol @@ -0,0 +1,10 @@ +pragma solidity ^0.5.17; + +interface WETH { + function deposit() external payable; + + function withdraw(uint256 wad) external; + + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); +} diff --git a/protocol/interfaces/yearn/IController.sol b/protocol/interfaces/yearn/IController.sol new file mode 100644 index 0000000..6f38e5a --- /dev/null +++ b/protocol/interfaces/yearn/IController.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface IController { + function withdraw(address, uint256) external; + + function balanceOf(address) external view returns (uint256); + + function earn(address, uint256) external; + + function want(address) external view returns (address); + + function rewards() external view returns (address); + + function vaults(address) external view returns (address); + + function strategies(address) external view returns (address); +} diff --git a/protocol/interfaces/yearn/IConverter.sol b/protocol/interfaces/yearn/IConverter.sol new file mode 100644 index 0000000..df07978 --- /dev/null +++ b/protocol/interfaces/yearn/IConverter.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface IConverter { + function convert(address) external returns (uint256); +} diff --git a/protocol/interfaces/yearn/IDelegatedVault.sol b/protocol/interfaces/yearn/IDelegatedVault.sol new file mode 100644 index 0000000..5d9bb44 --- /dev/null +++ b/protocol/interfaces/yearn/IDelegatedVault.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.5.17; + +interface IDelegatedVault { + function token() external view returns (address); + + function deposit(uint256) external; + + function depositAll() external; + + function withdraw(uint256) external; + + function withdrawAll() external; + + function getPricePerFullShare() external view returns (uint256); + + function claimInsurance() external; +} diff --git a/protocol/interfaces/yearn/IGovernance.sol b/protocol/interfaces/yearn/IGovernance.sol new file mode 100644 index 0000000..f0573cf --- /dev/null +++ b/protocol/interfaces/yearn/IGovernance.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface IGovernance { + function withdraw(uint256) external; + + function getReward() external; + + function stake(uint256) external; + + function balanceOf(address) external view returns (uint256); + + function exit() external; + + function voteFor(uint256) external; + + function voteAgainst(uint256) external; +} diff --git a/protocol/interfaces/yearn/IOneSplitAudit.sol b/protocol/interfaces/yearn/IOneSplitAudit.sol new file mode 100644 index 0000000..87edf28 --- /dev/null +++ b/protocol/interfaces/yearn/IOneSplitAudit.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.17 <0.7.0; + +interface IOneSplitAudit { + function swap( + address fromToken, + address destToken, + uint256 amount, + uint256 minReturn, + uint256[] calldata distribution, + uint256 flags + ) external payable returns (uint256 returnAmount); + + function getExpectedReturn( + address fromToken, + address destToken, + uint256 amount, + uint256 parts, + uint256 flags // See constants in IOneSplit.sol + ) + external + view + returns (uint256 returnAmount, uint256[] memory distribution); +} diff --git a/protocol/interfaces/yearn/IProxy.sol b/protocol/interfaces/yearn/IProxy.sol new file mode 100644 index 0000000..8b91303 --- /dev/null +++ b/protocol/interfaces/yearn/IProxy.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +import "@openzeppelinV2/contracts/token/ERC20/IERC20.sol"; + +interface IProxy { + function execute( + address to, + uint256 value, + bytes calldata data + ) external returns (bool, bytes memory); + + function increaseAmount(uint256) external; + + function withdraw(IERC20 _asset) external returns (uint256 balance); +} diff --git a/protocol/interfaces/yearn/IStrategy.sol b/protocol/interfaces/yearn/IStrategy.sol new file mode 100644 index 0000000..5b4924c --- /dev/null +++ b/protocol/interfaces/yearn/IStrategy.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface IStrategy { + function want() external view returns (address); + + function deposit() external; + + // NOTE: must exclude any tokens used in the yield + // Controller role - withdraw should return to Controller + function withdraw(address) external; + + // Controller | Vault role - withdraw should always return to Vault + function withdraw(uint256) external; + + function skim() external; + + // Controller | Vault role - withdraw should always return to Vault + function withdrawAll() external returns (uint256); + + function balanceOf() external view returns (uint256); +} diff --git a/protocol/interfaces/yearn/IToken.sol b/protocol/interfaces/yearn/IToken.sol new file mode 100644 index 0000000..c45ca62 --- /dev/null +++ b/protocol/interfaces/yearn/IToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +// NOTE: Basically an alias for Vaults +interface yERC20 { + function deposit(uint256 _amount) external; + + function withdraw(uint256 _amount) external; + + function getPricePerFullShare() external view returns (uint256); +} diff --git a/protocol/interfaces/yearn/IVault.sol b/protocol/interfaces/yearn/IVault.sol new file mode 100644 index 0000000..8376d39 --- /dev/null +++ b/protocol/interfaces/yearn/IVault.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface IVault { + function token() external view returns (address); + + function underlying() external view returns (address); + + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function decimals() external view returns (uint8); + + function controller() external view returns (address); + + function governance() external view returns (address); + + function getPricePerFullShare() external view returns (uint256); + + function deposit(uint256) external; + + function depositAll() external; + + function withdraw(uint256) external; + + function withdrawAll() external; +} diff --git a/protocol/interfaces/yearn/IVoterProxy.sol b/protocol/interfaces/yearn/IVoterProxy.sol new file mode 100644 index 0000000..39cd827 --- /dev/null +++ b/protocol/interfaces/yearn/IVoterProxy.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface IVoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + + function balanceOf(address _gauge) external view returns (uint256); + + function withdrawAll(address _gauge, address _token) + external + returns (uint256); + + function deposit(address _gauge, address _token) external; + + function harvest(address _gauge, bool _snxRewards) external; + + function lock() external; +} diff --git a/protocol/interfaces/yearn/IWrappedVault.sol b/protocol/interfaces/yearn/IWrappedVault.sol new file mode 100644 index 0000000..3b7b1c6 --- /dev/null +++ b/protocol/interfaces/yearn/IWrappedVault.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.17; + +interface IWrappedVault { + function token() external view returns (address); + + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function decimals() external view returns (uint8); + + function governance() external view returns (address); + + function vault() external view returns (address); + + function getPricePerFullShare() external view returns (uint256); +} diff --git a/protocol/merkle-distribution.json b/protocol/merkle-distribution.json new file mode 100644 index 0000000..c10ce24 --- /dev/null +++ b/protocol/merkle-distribution.json @@ -0,0 +1,3766 @@ +{ + "merkleRoot": "0x703278b5f92549a730583e87fa9f3b4f0e7cff1681e0ab46bea40d7856c1db57", + "tokenTotal": "8781000000000000000000", + "claims": [ + { + "index": 0, + "address": "0xccf6c29d87eb2c0bafede74f5df35f84541f4549", + "amount": "1103394117000000000000", + "nodeHash": "0xa66a0a5085b26da0af3c23a257f69275a0ecf383f8c36d1aa0e9f9d2c0b8cf55", + "proof": [ + "0xa53592f4412f71276a83c982986969a529dc610f004a7497206d4be8f569cfdb", + "0xcc4023ee0bff29d034428c1b6e06c876e41b60746f8e5d3941ff732d1da952d7", + "0xdd3426b4e15a8f4fd901853093b9b07e6e42020adaa75277dc0147e59a0f8290", + "0x112c5e47475d52c67bc98d675e1056afe3e6623117b94507e29be69ebf20a435", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 1, + "address": "0x25431341a5800759268a6ac1d3cd91c029d7d9ca", + "amount": "660901965000000000000", + "nodeHash": "0x5552493a56ad6f1ac4a7d42f36cc6c8b1ecfa6bbfc09e1f14824e581b86cd5db", + "proof": [ + "0x554d06155b8ccf45cd1f2f293f8eabdacc0e0b423021837c9c5035b5d9d758a6", + "0x2f92db0d3e17968df7094e69f99c51194c9236a1bb9f30ae59bb1124142b4b21", + "0xa29dab6c266c58115bc9610428c6698333af363ab59f695850636c30b506a079", + "0x58353663046a578dd1f9997dc38a4b7831c0a0b3796a5c26135ad8e0b4324c80", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 2, + "address": "0x4b502a08bc54c05772b2c63469e366c2e78459ed", + "amount": "1297902048000000000000", + "nodeHash": "0x7116fca76c57c84d452ddec4d9426424c7485bb485bc4861b67f1c6cfdb9a85a", + "proof": [ + "0x6b50fd7805af8a3f57178fb097a093382b02d4147a1d8f873747bc1ab3757b96", + "0xc38a179387b3002e62635025bd39ec83af3864e7000e28d7183dd767c3e15891", + "0x428c48ad33d483236e7b022672c662d2cb14b43b2cc565e3cf513754d2973895", + "0xcd656b995bfbb006bd6cc7e0553e7b43a8a428952ce7e40550495ee52cd3d9f7", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 3, + "address": "0xfbd50c82ea05d0d8b6b302317880060bc3086866", + "amount": "413348013000000000000", + "nodeHash": "0xa53592f4412f71276a83c982986969a529dc610f004a7497206d4be8f569cfdb", + "proof": [ + "0xa66a0a5085b26da0af3c23a257f69275a0ecf383f8c36d1aa0e9f9d2c0b8cf55", + "0xcc4023ee0bff29d034428c1b6e06c876e41b60746f8e5d3941ff732d1da952d7", + "0xdd3426b4e15a8f4fd901853093b9b07e6e42020adaa75277dc0147e59a0f8290", + "0x112c5e47475d52c67bc98d675e1056afe3e6623117b94507e29be69ebf20a435", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 4, + "address": "0xc301cd9448dc174a6c348f0f45eff27f523e5c43", + "amount": "211104021000000000000", + "nodeHash": "0x83413dbaaa6fcafe53c43c215e9cb86303df838a00400383b08d573e2e9bf722", + "proof": [ + "0x84c3bbd23b7bc4d625020aa2c347b902fbb26bceff9845a02c01ae4cd3177e17", + "0xee1183279d3d281f008fbdf3cb35a606ae474e0a5673450c7a56c2a43c57a11b", + "0x9ee727d3ed6f58402a106625a4d4fd294c3e9046de7e06e40540f5ca498d0432", + "0xde8a996044401b7617b89b4add1c4cfe27060c1b474a658abc3cbe0c0ca542cf", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 5, + "address": "0xd18001f022154654149ed45888c9c29def6d3ce6", + "amount": "207960423000000000000", + "nodeHash": "0xabde5cb713847f282e123d47feb2d1e774c005e4825ea22d94cb69c1bfad44dd", + "proof": [ + "0xabc3de0fb2fa4a248eaab7a56227e1b0bde4a2b982205bb5ca52304807d754fc", + "0x7c790d3433f68cde7a1ae87e232768dea7bf38d01e49eba530eedab0441c541a", + "0xe503f1f66922357ad84f71a8cf0ee12029365883c7f8b27daa7791e6665bf8bb", + "0x77aac907780e1eb7ed08bc6fb93c7a472125d7cd0e8720e5b60c436f725d4b01", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 6, + "address": "0xcde62da282f2f027f772f92719ee79e8fc01d9eb", + "amount": "189019806000000000000", + "nodeHash": "0x35e3a4fb47d502a455448dd9db9412ba51331b1c8027e51cab9735ac6584cbb5", + "proof": [ + "0x322c42f26d9bc1a993a977fb9c13de76b8c6a9e3bad315c2f3739e4f23e0a80a", + "0xdbc5b17868611035ed7b5fa15c9c514cb16c62fe167523d952200d0694290ae5", + "0xf7478ef32da8f4830df5edcd72303756fd6f171973be2e49aea5212970a2273a", + "0xb149db133f2de53fa1c5bfb7817947aa4b91fba91a7f427e3cebc44a51c0bb8a", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 7, + "address": "0x348a05e045b7671cec920252f737540d97b7fe31", + "amount": "161412342000000000000", + "nodeHash": "0x98b68aa8d362dc3d5d70ab7758ecdb7438d306d1a5f5852602feca94c85dd5b7", + "proof": [ + "0x97c6c981281aa38a2f04602572b08ea7b5ba778e148b17beee8da2a18ad11938", + "0x2d2d1ce318032af693f0fffb3731d73776c58111559cdca464596dc0e30e2535", + "0x80d3320efcf13447783dc7c21219a96e1b9acfb649c33c72903af272ff3b6f25", + "0x0f67eff6e6b3718a60f4c58f3ee545014e05194beff2f3e75751393773068ffa", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 8, + "address": "0xc13c37c6a3b077ef147c750a310cde3635ac7c49", + "amount": "160982073000000000000", + "nodeHash": "0x19e94c72e7c03046107a7e8afe7c1a9540c0388f704ef45c5bab1a12b6ab324d", + "proof": [ + "0x1ab1c669f8f18659e7a9f8361113d830b307aab9b0ed04e9c98cb823381c6389", + "0x5752e1714d111d5fccd65e492fc53070c7379e731749dd24f7f22516fdb012a3", + "0x52f131eb5f23b186983cc1103bdf44f5286b58cc5da6da8de3fa399de2a0459f", + "0x401b4840cfd7296b17a0bb231a8ee94c8c8fcd710eddb6978d46e0faf47d8eef", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 9, + "address": "0x537bf75de19f3d229e3a9018ee1a23c0c9c7d39c", + "amount": "128097228000000000000", + "nodeHash": "0xbea10368ac8a59ff0a3e7effaf767fe9e18c16e729401f698f3fe46bb7faf528", + "proof": [ + "0xbe6cb0716b81670ab26f75d178cd1015ee51c8799507680af2515aab4c8e9cca", + "0x437ec4f889e5185767eaa32b6e90d9e4854979b2d53677d64186fc7b985c23ef", + "0x77e041cf8797abfd574bda37e895763c1ccf70576b37398519e2c56b0a5698af", + "0x31d945c0d30504036c02fe1f0dc3952db23c29e3d87e59d1a06565ce6dae2924", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 10, + "address": "0x41bc7d0687e6cea57fa26da78379dfdc5627c56d", + "amount": "164880837000000000000", + "nodeHash": "0x8c84f088e53c994ed245357214efdb8acb0c742fbd68061018fef6a62de3aa8a", + "proof": [ + "0x8be98a9a35585e8dc57f4968eee19ad7ee0e42a7c4c2ea82d84866b2a56e4326", + "0xea4ff5fcc7c0fdb07adc0c92de6c0a2dc4d3c9c189b38b43f4f9f1d8c7910c1a", + "0x70320b540506f9453fa1e4f3dcb5f67f9cf8ea4c194e04076144154651c71b7c", + "0xcdc7c986bef6799a7b7aed7a38307c79b834bfb880319aecc678d4f1a76f3473", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 11, + "address": "0xb0e83c2d71a991017e0116d58c5765abc57384af", + "amount": "161868954000000000000", + "nodeHash": "0xaf0c3f1eb99a3f67796d0efcdab3180514928125897eee54f14740df85f1bae9", + "proof": [ + "0xb19015b5bb6d039d5f21876470631ada1bce518e8c9c758821ddfd7e91a20cb3", + "0xf6289c4c79b07ffceb612deb5609ccd9ad5cf10fbfdb6ee13dfe1156497b17c0", + "0x7a5ff3f19b447c6cd8d97470d640993dac0e19acd049d11248b6c33752696a5e", + "0xc9285e31e87d3c235fc0dc4ab5c5978891a414a32a26c69b43d5834adf7fea91", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 12, + "address": "0x983ec6d045713d1b87f110a7edab9fc7996eefc0", + "amount": "92332215000000000000", + "nodeHash": "0xd8b1c8f8abdd300f7afdf4a1d5df9cd9033e1ffb94e3fa431607b3ec3e564a3f", + "proof": [ + "0xd95c3e11f57be37f38c4cbd492ae5b3952e5de09e3fa8aa948842812d2347267", + "0x0e52d7d68649479df454383601d11e4b54b202ba1e5a3d3f6d77e8bf11d60bd6", + "0xab2c221ee4dd5e1ebced8623292417356a6da943c17720d5865ae17a0765e4d2", + "0x8c48e5104ea64540b9742e7e67c632558ee596ef39aef3dabc5a750b5a0d8de1", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 13, + "address": "0x03c322db3f0d92ccdf6f8b6effd4031c94bed1ab", + "amount": "85289853000000000000", + "nodeHash": "0xabfb51db5246b5da1b854bfce08a5b014e1d258a113a360f2b2ec1b4b3fcca1d", + "proof": [ + "0xac1f333c32a9f1f183ee111e38850a588a9c36a995838837789c299a1c70abf8", + "0x5df9cd5169dbf95d7d68d5b69af3c5d18986d7c4e40f77c9b1cda186475c9293", + "0xe503f1f66922357ad84f71a8cf0ee12029365883c7f8b27daa7791e6665bf8bb", + "0x77aac907780e1eb7ed08bc6fb93c7a472125d7cd0e8720e5b60c436f725d4b01", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 14, + "address": "0x68c88fe8fee367d64fab809621c44dadf9bec2b7", + "amount": "84745431000000000000", + "nodeHash": "0x5a598f41d51165d456db10444293183dc37ad6b82ec7ebdae2185f0d215b51af", + "proof": [ + "0x5a9c9df89a392afc1506f578746e2a07fe8d9ed57bef284c3a03ca66095f62dd", + "0x8ad619875948d9ec90624e097d678c60d61a4d105ad7119c54ff600e7582a53a", + "0xfb6b61993f883176c7796e3c8f60ed9e153ddf14ee510fb2e75e88c610562b34", + "0x58353663046a578dd1f9997dc38a4b7831c0a0b3796a5c26135ad8e0b4324c80", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 15, + "address": "0x0c5a2c72c009252f0e7312f5a1ab87de02be6fbe", + "amount": "79590984000000000000", + "nodeHash": "0xa500d5b5bc87cd16db273611b103367bc3e78f272b4521a3e344eca724a39e3b", + "proof": [ + "0xa3b291cde829f82c45a4b1d4e038ea92bbc704beaa5d987d0688d946c82ddcd8", + "0x16e69caacd8ffd43e2913bfa0c48e6ef0b745d7912fc67043ff2afa1aa2a8937", + "0x2a04f687325c7538cb6f084fc767c5ed72dac0dad6a28ef23986fbcea61df2e0", + "0x112c5e47475d52c67bc98d675e1056afe3e6623117b94507e29be69ebf20a435", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 16, + "address": "0x0bae7d98e1d67741bfcba5d90b17a9856b1bf82e", + "amount": "73813086000000000000", + "nodeHash": "0x8b361f814a7b3d5f57028f091b9a89b596243810d14e3c7435f6a14885e24ff6", + "proof": [ + "0x8904c226484fc9bcd5cf04c8836b57cdabe792f22adafb2675887f3368425532", + "0x4c4fbc32f05440226a7cdb7a0f22e1863eed37c19212fd9becda0b41c4527888", + "0x3697a1b81c3b046b0a765a73489c36042e120349ca90156ef89e0003c12bfefe", + "0xde8a996044401b7617b89b4add1c4cfe27060c1b474a658abc3cbe0c0ca542cf", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 17, + "address": "0x289c23cd7cacafd4bfee6344ef376fa14f1bf42d", + "amount": "75200484000000000000", + "nodeHash": "0xd95c3e11f57be37f38c4cbd492ae5b3952e5de09e3fa8aa948842812d2347267", + "proof": [ + "0xd8b1c8f8abdd300f7afdf4a1d5df9cd9033e1ffb94e3fa431607b3ec3e564a3f", + "0x0e52d7d68649479df454383601d11e4b54b202ba1e5a3d3f6d77e8bf11d60bd6", + "0xab2c221ee4dd5e1ebced8623292417356a6da943c17720d5865ae17a0765e4d2", + "0x8c48e5104ea64540b9742e7e67c632558ee596ef39aef3dabc5a750b5a0d8de1", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 18, + "address": "0x83d5520fcd618fae372ef597acdaabcad6871295", + "amount": "99392139000000000000", + "nodeHash": "0x6591937ab2f9d12654f5f20f6a6f6d1d91ce19dd894d496a8d454a59fba4ffdd", + "proof": [ + "0x64e5c55ef03b40dd1da750150cd4d39fcce3c48857667b5cf12ab2dc4ca0e7b8", + "0x7cfa616b6213a643562e2be4b086eead77c318e23db11782c0bfeaab8e366dc8", + "0xd2aade99c8f850b3a3bbe9cf35336218f89132a869dd4c4641d5e9f5bfee5f9a", + "0xbef3adfc06e9c1b99f0036e6bd56baa2f939d20e0f53ee8ccdaf789586255173", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 19, + "address": "0x82b8b659a4a98f69cb7899e1a07089ea3b90a894", + "amount": "87889029000000000000", + "nodeHash": "0xda86eb0650d7c85c40bb685e89f8560daa3b7ce910638ceb9e341103ef5f6030", + "proof": [ + "0xd98132967f4213ae7d40892abed5886f93e86167425f244c292d0070e322770a", + "0x0a6fdbef907c63b8b31337c51d31be9b4c94b8c624534eab843080b4ba13d492", + "0xab2c221ee4dd5e1ebced8623292417356a6da943c17720d5865ae17a0765e4d2", + "0x8c48e5104ea64540b9742e7e67c632558ee596ef39aef3dabc5a750b5a0d8de1", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 20, + "address": "0x34075fc94358bb3a231105f3f8ae663c3960200c", + "amount": "54591477000000000000", + "nodeHash": "0x0f574ee681925a9c69497c6042e01fe9f219a65f4a23ba48bf5b0d331cbebcde", + "proof": [ + "0x0ef4f9145ea91c5bc1afdea4c8c4cae3f448a697ee2c7efe080c533326eb9e70", + "0x61f53fec17bce18fd0593855b0bff78ee1a8dd86238ef652eec0f1ae248559ee", + "0x27f5ba055019acd91796b1eb1f12fbaa9b667c42bb565c2b0aab0809e3752977", + "0x3568ae0dd2327f26f3bd4c1af82d9a7d8132e50855cdc1c20f276776264d6ab5", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 21, + "address": "0xe99ccf02e3a3671ce4e3c176ca10a17c7c0dd2ce", + "amount": "52361103000000000000", + "nodeHash": "0x0df65bc43b277750168597930bdff98b96ca64526648ee2ffa3d9caf6faeab1d", + "proof": [ + "0x0ea0a7b2bfef2b32b474f642d4c1e7e75bc49b47317a9259954a1172792ff210", + "0xbb473b4b3dcc2b076983245b5d9817b98cc51377bc166b349b1b249646ad24af", + "0x27f5ba055019acd91796b1eb1f12fbaa9b667c42bb565c2b0aab0809e3752977", + "0x3568ae0dd2327f26f3bd4c1af82d9a7d8132e50855cdc1c20f276776264d6ab5", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 22, + "address": "0x2e945f5229dc19accaa8568ea783cbe73aac1505", + "amount": "49937547000000000000", + "nodeHash": "0x9fc2cb5ba4a2610f57fa85ea806e2f53a82cbb1e20149dfabb3bf6421e59f897", + "proof": [ + "0x9ee70147ff673fb436fff590cc88df68d5d95796b8c4bf1be90a3243b857f055", + "0xc5fbf020a95ce99d05b527b5916691f9495abad0909f9b5f934b7695f1bb149c", + "0x99915e59da35c21cdd6c6166d0106f8d8f42d9217c368ff7212a04abf0f34299", + "0xfc494fd4d56122448384ef08f6ebb78947a312c2e1369042b28819274e2acf66", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 23, + "address": "0x55e1602c77e762742d69dbc57ee5f016db83f2d5", + "amount": "54187551000000000000", + "nodeHash": "0x245eeef83b1665e81590fe99607b4044dd22fa66c5810cf62db42daa276018f0", + "proof": [ + "0x2127aa5e2e753afc65b44f48745490a96653895f0ed062e61d0a1c3d16996b72", + "0x0ea261dfafa4d2569553c51c317b3d522843271443a3d425f82cba4c331dd15b", + "0xbce9650f2ce0bdddf4d157210724cbdd8249457433191a81be341fcaf6c64a30", + "0x1d9b0c09a3cb63729974fa7589d3e021eba7ef36ed2ec96be004ae76e40e4456", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 24, + "address": "0x280bf69d522bbcfb3aeb138c59d85a16e449057c", + "amount": "74085297000000000000", + "nodeHash": "0x7907d24968c9858254fccbee98997f19c79164cc9df3e5d65571bb47969ed2ee", + "proof": [ + "0x79093ef1d89e28c56c31ede158d644c3d3fbb5ad6ca8f2f46287197560f194dd", + "0x3230d530998fa5a1d112dbf7b97358852723757a900467d431842f692c0646a0", + "0xa2954bce93ae1f34dedb0c78d5f07a45d696775993a5716f85f184e6114bf588", + "0xe92595020625bd816794a0573140fc5d9e5dda15bf5439f01004d872a3811f20", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 25, + "address": "0x302218182415dc9800179f50a8b16ff98b8d04c3", + "amount": "45450456000000000000", + "nodeHash": "0xfdb9ae7334e8b0102e2b0cdb16a60fc5b2b651cd86711baf3ab927b0395f3dff", + "proof": [ + "0xff8a6d8cea89c87f8a2ffefcba2a3b026248ac5c1fef2cb4ef8677731a884fa2", + "0x049b5b005b9b66803cbf02600ea7719af80de201958c535f372f1cc3b2e60bd2", + "0x15e1e45a6fca8e1c8e57ddc39a2962d2260850882b5c8b04832d1dd821cf9e27", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 26, + "address": "0x4d1467c312a83794c99b9db678dfdfa743394ef7", + "amount": "42219048000000000000", + "nodeHash": "0x535b3fb73668f45e89bc54a02b08c3f89376b64f771fdb9acf3dfc659c48f52b", + "proof": [ + "0x534176960099e6261929e0e7ebc89dccfe3c3aafd09143811a55fe303e5ad8fc", + "0x996dca66a8f668fa948db0550dea9c2bdb3e8932ea7e7f42b7ac4b6c1037b078", + "0x4d72bdbb948be8a9d3a660d43eaefb5cccc88ede0e152cd8fed46d934ad5d0b2", + "0x1d51ca0e19aa543a6545b29cab8ac5f9483bb15f0c024a81b9aadfeb29b9f54f", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 27, + "address": "0xb6ca5d1ed0ca4bf8444c9dee2d068a9f4c5e2e92", + "amount": "37328031000000000000", + "nodeHash": "0xc1d897570302cd63543e34325ebb8776dcb2ba02098752124d98834c07c7595e", + "proof": [ + "0xbf491f133480a9267e7965111821b718d141d562b36eec29bd86aaccd3013652", + "0x30a8d6141d357aabf88c9d3208d43fe9c32b95a9adfcb1b1afb67096fc29b113", + "0x77e041cf8797abfd574bda37e895763c1ccf70576b37398519e2c56b0a5698af", + "0x31d945c0d30504036c02fe1f0dc3952db23c29e3d87e59d1a06565ce6dae2924", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 28, + "address": "0xc9f9f6cfd655417acb8d1bb00fe77ad9a6d9ca81", + "amount": "36326997000000000000", + "nodeHash": "0xdc6cb6dba2b5e65d16b2744f9bb5d980137bf74bb078e2f8e4e957ee295741f1", + "proof": [ + "0xdd36dddd34bcd95c3c9fc793abdd648d934ea47cd2c135d18aee236a680c8cfe", + "0xcd97bef637f58ff492b42f118552eda50c0020513defe22bd7b9c9822afa32d2", + "0x12d18da5dbeb6a18115947a6a933788d9f4fe3a2d151d55e55a533ecb360888c", + "0x2b7b07c087af29ef8f3779ca825e620e6a91fc1b8119a477cb9747b5bfcbe5c0", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 29, + "address": "0x6af235d2bbe050e6291615b71ca5829658810142", + "amount": "36054786000000000000", + "nodeHash": "0x31107d68b1e228215b3fa333c9d48984427de785a99d56f04640a6542b236382", + "proof": [ + "0x30572f5c30ab4086c014cfc33461371e2ce8cad8e3cc9e0a71781be74d28b65e", + "0xdb43f9b35e0d6b2d855e60ca6191e14184a51d855bcc0e057de1ba8b1be550dd", + "0xae70dc4f1661eb4cd5ce6ce92d37555b22131bab4d1e2299a12a96bb693592b8", + "0x3f9c8e77affbca217664af93969c37b75db5f5e970d56dd0a2f7b374a9d8c12b", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 30, + "address": "0x380ec413b303741821fec3c2d11043965dbe9948", + "amount": "59131254000000000000", + "nodeHash": "0x1ebcfb87f058e147f7af10b30dff3aba727adbf49a1988697c80c3c3d214256d", + "proof": [ + "0x2054ec96506440bef9e73da667d94d714e17f667796e533d94b08ec1593ea89b", + "0x99c6f8f0df2e40cd0d8419b827cf8c1dde54e98732e7d6e043e79368f96f1ad9", + "0xbce9650f2ce0bdddf4d157210724cbdd8249457433191a81be341fcaf6c64a30", + "0x1d9b0c09a3cb63729974fa7589d3e021eba7ef36ed2ec96be004ae76e40e4456", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 31, + "address": "0x33e3c007d1d48e2b645c9ce22570267b0c82f578", + "amount": "32041869000000000000", + "nodeHash": "0x3989d9e21f1ec41b87170c422434d91efaa005618f10bd8a8d033fd87f25c958", + "proof": [ + "0x377a520a3ce13c13e3990444ea926cd2a6910c03a34dac22acb1ce7f5fc5f094", + "0x684ccde3c00bb5dd1a27aa3862d5c93f331f6f8a2ee21db65d808f434d2ca075", + "0x9ee3fa7c938654616c76fc487fceeed95be956662b33deb34a3b9c277f0408f2", + "0xbd97d3672cdd0dce515b28377cdb7ddfc5d08d2a126f287bf0f31ad87296aab6", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 32, + "address": "0xa92dda19aa92bf20867743ee631afee83524d835", + "amount": "44010372000000000000", + "nodeHash": "0xd22143e34b5e3572d98fac263e8e1cd673d541c6cdfd33920a54fa76241c258d", + "proof": [ + "0xd2ece52f60937e0182db66c1c28b3c6637175da901aa1290e88fea06f2e853b7", + "0x8a76e1fb3209d3100160d93a8b364ec2fb9c867bcf0d0f69e0974cc4bc0aeb4f", + "0x888604db6f7d9a68ced6fff7cea8cdadeffaa0a505b7b4f019c88c28947bbb29", + "0x8c48e5104ea64540b9742e7e67c632558ee596ef39aef3dabc5a750b5a0d8de1", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 33, + "address": "0x1302c202ef727fd656cb6356ef6e8b3aab64aa58", + "amount": "31400856000000000000", + "nodeHash": "0x8fc84c91276968608ed2ec48b87f81a10d01dfb03624e711d6415f7ac90e78b5", + "proof": [ + "0x8fb95874299ed3eab52c4e4221140ec32d0bec00e4fdd6a0ede8dd08b441c508", + "0x0c267ba709fb12206ed1ed8f2699e2f909f6e627cb304771e2e03bb89e07e1a5", + "0xaa377edb22f3b6832c85b39443e2addce35609872bf62447d5b482d94dc69eff", + "0xcdc7c986bef6799a7b7aed7a38307c79b834bfb880319aecc678d4f1a76f3473", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 34, + "address": "0x8b4bddaa976996f3ee6dd07851095351e23ab90d", + "amount": "26430810000000000000", + "nodeHash": "0x9fd94db8ff650eaf61c6c7b5f1f6258158dd33a09ac498e2a8b190169bf083e4", + "proof": [ + "0xa01acdde1ca8ae43a98e53187d2cb65849688fd99edfd4441b50afb8a988bfc3", + "0xc6a6bec0cd3c286e22a1e4f95151526f0f85f9a7f8dc72e4af2e287a777cca81", + "0x99915e59da35c21cdd6c6166d0106f8d8f42d9217c368ff7212a04abf0f34299", + "0xfc494fd4d56122448384ef08f6ebb78947a312c2e1369042b28819274e2acf66", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 35, + "address": "0x44cf78e365a3cffed54b7f2d702c2c6239276fa3", + "amount": "22883286000000000000", + "nodeHash": "0xff8d7e249f14062bc711636c7ceb62cb0f76fc58478f9fa620cbca674c156843", + "proof": [ + "0xffc360b46db845430db7e05bf69f584ae13d480c82f54670a185c411f93db380", + "0x4e4eee08e165e99128ac39e0aa923556cfc2994233b29b9df81e1ab1acfa3eaa", + "0x15e1e45a6fca8e1c8e57ddc39a2962d2260850882b5c8b04832d1dd821cf9e27", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 36, + "address": "0x95801473b2429dd9548b3a9550b8bb9963a3a695", + "amount": "22049091000000000000", + "nodeHash": "0x0acc83080d8254fa3e8153373c34cc15d5685e352135766b17f1c0a70ed70674", + "proof": [ + "0x0b83b157c8d22b101f2b07161fb9b32290ffc22155dff1286d53d4b1d1750c6c", + "0xc75e7cc56c9524dc75b3c19ca4e99a17e7dc9aaba3d423ffdbeed59b260781c5", + "0xc7e6a378be7722288c6d0e53e9c3c2681b948dc95aaa85fc9e61e4385b30260f", + "0x99927aaba68fa1c526c8be885fed2a583000a016f39e6210564017b20cc96842", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 37, + "address": "0x9166be1f159314f19f59684713ff17221ccc9a17", + "amount": "24692172000000000000", + "nodeHash": "0x973c4864a71491b9e53f89770a198ec0d05aa7b92dd26341f787483a3ac31502", + "proof": [ + "0x964e44634a3453cfc476a9b587e180460b47111fa94c8745d548e392aa3aca28", + "0xf0061c6c70cfa67cbb6c3a194e6e9f8de3e5143a763b91f5c869038b912d41ba", + "0xf511f28e9f90c67ec4ecd3d631874d8e9007bcf1f3e60d014b8f14f0091d93d8", + "0x0f67eff6e6b3718a60f4c58f3ee545014e05194beff2f3e75751393773068ffa", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 38, + "address": "0x01eabaa6b6776f569cf5cc85c71eec096c795779", + "amount": "26439591000000000000", + "nodeHash": "0x153b5d57c32a01c9ebea820468594d6d18d98eaab99af1950c5c65068c59185d", + "proof": [ + "0x15b1f25d8222a0a790d4e9f8155385ceb11f665aa9539eca7b47b2d7043d9448", + "0x6c294dd472dfe45c9fc8ef3b2f6e24a9d5cafdbdcb1124da7041c9955bb3c750", + "0x0e31324d8f7bdec33bc699b1ac845b07055f9836b7852f51934cbaca50f4967f", + "0x401b4840cfd7296b17a0bb231a8ee94c8c8fcd710eddb6978d46e0faf47d8eef", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 39, + "address": "0x50664ede715e131f584d3e7eaabd7818bb20a068", + "amount": "21741756000000000000", + "nodeHash": "0x9cb73a7a4515d09379322ea6fc70fbc2445b2f2bc58357f6db2a8fd7513b7fd0", + "proof": [ + "0x9bfd0dd73cd00659f7ffaa43583ece659643e456c1727fa5104630664e712ac8", + "0x20860ec6f1a2509db3c3cfcbfa60b75c123224cbed4f95de237c94a2e711250d", + "0xcfb31d36e8200eb045fe23af2958fcf64e7c29d370930d2f986db9135dba31ab", + "0xfc494fd4d56122448384ef08f6ebb78947a312c2e1369042b28819274e2acf66", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 40, + "address": "0x4280615f4607492523b496a3d70e22ee44129d40", + "amount": "64250577000000000000", + "nodeHash": "0xb3c688ffda0e6a31abc67ded87a2dc5ab226c3f5f98854884b299e64cf1e1609", + "proof": [ + "0xb1be9ee1d722771394b363bcaff4210ad413be016004f1b07ab26e905fd0c894", + "0x9d3ccd9ebe1be04d0b3a6d06b85581c4958cdc3f1d5e2fdc1f24da87afc7abda", + "0x28f06e02ae1339fc54b9c964d1b5ad118e18997a61fcf36511d31cdf927cc508", + "0xc9285e31e87d3c235fc0dc4ab5c5978891a414a32a26c69b43d5834adf7fea91", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 41, + "address": "0xff694ba66db263cd0f2f4e14cb9916a3e60713d1", + "amount": "17763963000000000000", + "nodeHash": "0x36cc998d1c4d9b08b2c664e30e487113e495b4dbb58b6bfb29e32719bfad94e6", + "proof": [ + "0x36625ac2ced62938db2f9272088a86f060e6e48c84da114a64b7b112ecb2fde5", + "0xa95d27bf8afffd771be96a8e8f1a9a49699cac5ce1b246a2a197b0b6bb02a63b", + "0xffd4f9d79518701e9fbb847603851d8bcb3af375c38eb6e9de63aa69d5b362e3", + "0xb149db133f2de53fa1c5bfb7817947aa4b91fba91a7f427e3cebc44a51c0bb8a", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 42, + "address": "0xc579ef8b14774bd15c021d2c0aafd04bdd7dd137", + "amount": "17728839000000000000", + "nodeHash": "0xffc360b46db845430db7e05bf69f584ae13d480c82f54670a185c411f93db380", + "proof": [ + "0xff8d7e249f14062bc711636c7ceb62cb0f76fc58478f9fa620cbca674c156843", + "0x4e4eee08e165e99128ac39e0aa923556cfc2994233b29b9df81e1ab1acfa3eaa", + "0x15e1e45a6fca8e1c8e57ddc39a2962d2260850882b5c8b04832d1dd821cf9e27", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 43, + "address": "0x39979745b166572c25b4c7e4e0939c9298efe79d", + "amount": "17298570000000000000", + "nodeHash": "0xcc9c49a5b1f7ff9f554707e876d986eb96de2cbae4d45b8e77467d2f1bd35a06", + "proof": [ + "0xcc64d94c8158ac3c856582e86dbcf00b29461ba77121921cef3d807a0a8e2bf7", + "0x626b7860b684e172c34f58fba6d1f7fe69b87c6cc98d88a376adf9537e3d131a", + "0xba770e25357a76c9190bdccfaf3cfbc9f10ba8bcf8777d82c64997ab022c0e3d", + "0x049057c435e918361a347a9fe31a6f48819b657ce283c5390310bbc944753eca", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 44, + "address": "0xb74125df13cb9194d93d8b62e0db30352f2b8001", + "amount": "16885863000000000000", + "nodeHash": "0x26a57a02464b66f3d50c83dfab7ed2a636e06da38f0917079e0775670cfeb0b6", + "proof": [ + "0x296a58cd4e848c7c8e90b77a802ba45d9badac8be56b022b01e72f22493bbcf1", + "0xa23c11acf3af8d6541fe01115721a53cc02aa64c8711d488ea83aa785019c612", + "0x8b17088d53d8cd82f15cd19fd878ceb428963b30615a65fbe1afc21c9fac39d5", + "0x1d9b0c09a3cb63729974fa7589d3e021eba7ef36ed2ec96be004ae76e40e4456", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 45, + "address": "0x24403c7f45bd16f6dffde0c1c45792d9825fe9a6", + "amount": "15568713000000000000", + "nodeHash": "0x56054f809abf6ae70fbdd71ac235716a0e01195f877c18bce681ef67ff9231a3", + "proof": [ + "0x57d3b4419edd2ff472c8073e8bf675f7976fb491175d51406065d1e8fa7a323a", + "0x2ce401ea4f624752dc4f2611b2af6ed78386103d3e9b42a04648e679492f0f3b", + "0xa29dab6c266c58115bc9610428c6698333af363ab59f695850636c30b506a079", + "0x58353663046a578dd1f9997dc38a4b7831c0a0b3796a5c26135ad8e0b4324c80", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 46, + "address": "0x14647973d093256920ce9c6c8a6646b3a4e43869", + "amount": "14778423000000000000", + "nodeHash": "0x0c47dc7d3bb60a1e01119485736402bdb29815cd508670cd1c0eb3e6bd0949b6", + "proof": [ + "0x0d5a5081de2e78e5032898bfbcb3dd94739d34ef4f644c3b75bfa3ad5f173262", + "0xb963de4f4436b346233faa43b8ae655c28e903aec74775f8954e845d8a729412", + "0xc7e6a378be7722288c6d0e53e9c3c2681b948dc95aaa85fc9e61e4385b30260f", + "0x99927aaba68fa1c526c8be885fed2a583000a016f39e6210564017b20cc96842", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 47, + "address": "0x279322ca10987bb236902dbddb85e9e676cf7a0d", + "amount": "16429251000000000000", + "nodeHash": "0xee05e205e873d61918fb919f9722d079b03255874977054a3ccd55d3c73bfec7", + "proof": [ + "0xee8c39b82dd589f111f4b015501c6e890d7432754eb3140c815fdf4b02130c55", + "0xda393665de1c0e94aa5468a8de6c20900366fde499a56316c6fe3760bd40c712", + "0xbcb103468201a2c28e4f0287614283c7f71e3d3caccb0d84054ad77dd6f8268b", + "0x4374227d00e14e26a6659c58e26ba194704a9c4428299eefc28bfb5fbdf9f12a", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 48, + "address": "0x1f39e0a9efdce589f7c7eb0a6c0bfa400050b2f4", + "amount": "18694749000000000000", + "nodeHash": "0x3ac6158df44f651b423f622413095831b356ae735eb8d7748d5cfe2b06641576", + "proof": [ + "0x39e187da7c06c5bdedddca51726df59bd01a8c406c915cb5430ca9e776c869ab", + "0x48bd5c8baa8450c942ed65eb641cfed2882d14944565066fc28fd66d41e5cd2f", + "0x9ee3fa7c938654616c76fc487fceeed95be956662b33deb34a3b9c277f0408f2", + "0xbd97d3672cdd0dce515b28377cdb7ddfc5d08d2a126f287bf0f31ad87296aab6", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 49, + "address": "0x000000109781b8fe1eeec8fb4507aa64bfca5914", + "amount": "12758793000000000000", + "nodeHash": "0x1ab1c669f8f18659e7a9f8361113d830b307aab9b0ed04e9c98cb823381c6389", + "proof": [ + "0x19e94c72e7c03046107a7e8afe7c1a9540c0388f704ef45c5bab1a12b6ab324d", + "0x5752e1714d111d5fccd65e492fc53070c7379e731749dd24f7f22516fdb012a3", + "0x52f131eb5f23b186983cc1103bdf44f5286b58cc5da6da8de3fa399de2a0459f", + "0x401b4840cfd7296b17a0bb231a8ee94c8c8fcd710eddb6978d46e0faf47d8eef", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 50, + "address": "0xd4c80637f45a55b5e1afa9cd0c935395063d4523", + "amount": "12100218000000000000", + "nodeHash": "0x85ae1e64245e119c57c096d3e3e379c677135d7e25491a94c4472e3ea8d735e9", + "proof": [ + "0x851ab05a225a1373e0b64d9e5baa033c7786b7408a9a833c81881f495bff132c", + "0xacc504eebf4d3892781c5ffbfd444dcc9b41e288f1c11178a72718e1e50fb2d5", + "0x3697a1b81c3b046b0a765a73489c36042e120349ca90156ef89e0003c12bfefe", + "0xde8a996044401b7617b89b4add1c4cfe27060c1b474a658abc3cbe0c0ca542cf", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 51, + "address": "0x718fdf375e1930ba386852e35f5bafc31df3ae66", + "amount": "11380176000000000000", + "nodeHash": "0xfa8a131a9a52bd1a7e9e0fa8f26ada5c778cbd9b50f444d055c710e3b4481090", + "proof": [ + "0xfa88345761fe5fcc26108f1ba487777c7eb136120ef4e8fad2ee99c967ab534e", + "0xecffe4563985165d8dc5d9e448214d589acbef654ee8f2765bf2a33b28ff1dac", + "0x8a190fca6b4bf18ee70ee088347d1d5ce46e23942d97e6d45cff623f33b56a8a", + "0x878b93f5379152f0d671b155b7a722b05eb555558bbfe56bfbb394a485c7952e", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 52, + "address": "0xc2d201037bdf8f7fe905f1073106bf4385b65a6f", + "amount": "11178213000000000000", + "nodeHash": "0xdd36dddd34bcd95c3c9fc793abdd648d934ea47cd2c135d18aee236a680c8cfe", + "proof": [ + "0xdc6cb6dba2b5e65d16b2744f9bb5d980137bf74bb078e2f8e4e957ee295741f1", + "0xcd97bef637f58ff492b42f118552eda50c0020513defe22bd7b9c9822afa32d2", + "0x12d18da5dbeb6a18115947a6a933788d9f4fe3a2d151d55e55a533ecb360888c", + "0x2b7b07c087af29ef8f3779ca825e620e6a91fc1b8119a477cb9747b5bfcbe5c0", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 53, + "address": "0x8f6da831710a8d30de1111aaf28c697aaf5056dd", + "amount": "10607448000000000000", + "nodeHash": "0x6b50fd7805af8a3f57178fb097a093382b02d4147a1d8f873747bc1ab3757b96", + "proof": [ + "0x7116fca76c57c84d452ddec4d9426424c7485bb485bc4861b67f1c6cfdb9a85a", + "0xc38a179387b3002e62635025bd39ec83af3864e7000e28d7183dd767c3e15891", + "0x428c48ad33d483236e7b022672c662d2cb14b43b2cc565e3cf513754d2973895", + "0xcd656b995bfbb006bd6cc7e0553e7b43a8a428952ce7e40550495ee52cd3d9f7", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 54, + "address": "0xfeb041dec59236203606973b679248b0e4d3f91c", + "amount": "17087826000000000000", + "nodeHash": "0x363dc5ea5748610a62fcec1e6cae5d777feaab525d585dc9a19f84613f9d315c", + "proof": [ + "0x3605a2911eb719fc2c22fec90fdfc050fe189772ccb0eccd94d8ae89d388a9b5", + "0x3d8c7b4e9111b1fcce37580d6352360fd2a748e27c821dcb9e63c6bf24203c4e", + "0xf7478ef32da8f4830df5edcd72303756fd6f171973be2e49aea5212970a2273a", + "0xb149db133f2de53fa1c5bfb7817947aa4b91fba91a7f427e3cebc44a51c0bb8a", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 55, + "address": "0xd6137678698f5304bef86262332be671618d5d08", + "amount": "9931311000000000000", + "nodeHash": "0xc7f76a84e0a74051d67ae9f1b725f9b4b603690e50bf1bcf07dcc46ba6ab3a74", + "proof": [ + "0xc75ec40dbe45bdd4424e9a2c8746793957b9dd9bdf5758bab70d0616a178501e", + "0x7ea73309f2675c73d72dff2898e3ba796b71f0892aced82e0b582a4c1d2223eb", + "0x1de190c06efaa371a311ee112f45007771a2745d514c4f0209909371267989c1", + "0x8f1e0bca0b330c8a6044d203531a4fe59749a3d5ebccd1a4a524fff2f807ad3a", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 56, + "address": "0x67557095bad5dca7499ee7b40a71e030fe511322", + "amount": "10273770000000000000", + "nodeHash": "0xfa88345761fe5fcc26108f1ba487777c7eb136120ef4e8fad2ee99c967ab534e", + "proof": [ + "0xfa8a131a9a52bd1a7e9e0fa8f26ada5c778cbd9b50f444d055c710e3b4481090", + "0xecffe4563985165d8dc5d9e448214d589acbef654ee8f2765bf2a33b28ff1dac", + "0x8a190fca6b4bf18ee70ee088347d1d5ce46e23942d97e6d45cff623f33b56a8a", + "0x878b93f5379152f0d671b155b7a722b05eb555558bbfe56bfbb394a485c7952e", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 57, + "address": "0x4315ed306e03192867b1799e4c0895a4cd9d82d4", + "amount": "9623976000000000000", + "nodeHash": "0xeda7230e3380aa4f5a7571061a6e49c07b4dd87a470dd9e0fabe2b740f94977d", + "proof": [ + "0xec33e4324fae8d2e02f0fca6fae6df0f00962be817b4aa12f8dc1edc37c575a4", + "0x7ba3d59ab6597fb5e7133104a2335785257c5e8d76f1678f50ccd466bfb67472", + "0x31671f7e335400af5b05a1bb5bdfb79516d12b6b57500a1d7aa1e53bef10935e", + "0x3f59b8cdb34f4bc3b9c931471fa7d84b69826722d8401478e0c9afbc1f56fa16", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 58, + "address": "0x34d6508b607af072858e64c65ced6e7993225e32", + "amount": "9588852000000000000", + "nodeHash": "0xf8d5385c20078d9a9a66fa4e1b5a1033963d5e828f267a6aad9853ff77643726", + "proof": [ + "0xf92a1f17d43fe8171e77be455711461ae865e1c2b52d37c06ad9dda67fbad1be", + "0x1b3912228974000a20cd4cc195e0c6f83a56d09367d32d440ad027a03338a850", + "0x16dcfeef35a8dc7ec586c8ce9af48cbdf90a8fae3fc96ce8f613094b4835f4d4", + "0x878b93f5379152f0d671b155b7a722b05eb555558bbfe56bfbb394a485c7952e", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 59, + "address": "0x584baa4b71b0a3fa522658128f36a6a4abeac2ae", + "amount": "9439575000000000000", + "nodeHash": "0x59ff47e6181cea66f99065285738b74ae463362cbd80118af58719e40bd1d8e0", + "proof": [ + "0x57ffc5625b1e5e91776386096dbad49a252d862f4d0578dcbc62320a37565db1", + "0x42f6128e44456df432b83e004440e9b815662b4d71c563074a84c714196e8765", + "0xfb6b61993f883176c7796e3c8f60ed9e153ddf14ee510fb2e75e88c610562b34", + "0x58353663046a578dd1f9997dc38a4b7831c0a0b3796a5c26135ad8e0b4324c80", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 60, + "address": "0x4cae0b0e66a1c55137df4a1175c1a612d44592fe", + "amount": "31102302000000000000", + "nodeHash": "0xba4c10258cd4f7c83441449d78cb56fff89d87b52952e87a4038bc8d55020eef", + "proof": [ + "0xbbd0d88710ca501681d3d0921db16a6646187ca398a350d4e4196e577c4dd206", + "0x5e71ae954018bb5ce96f417374357bd0750722c2bb8b36e82158d6d40dfe5f11", + "0x4e2cf104325c508896b8c5cb332f174a17e5f8b29e2c5c58a1244e25d9ae856a", + "0x31d945c0d30504036c02fe1f0dc3952db23c29e3d87e59d1a06565ce6dae2924", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 61, + "address": "0x445ccf43489774214fbca297e0dedc11eda3334c", + "amount": "14681832000000000000", + "nodeHash": "0x8fb95874299ed3eab52c4e4221140ec32d0bec00e4fdd6a0ede8dd08b441c508", + "proof": [ + "0x8fc84c91276968608ed2ec48b87f81a10d01dfb03624e711d6415f7ac90e78b5", + "0x0c267ba709fb12206ed1ed8f2699e2f909f6e627cb304771e2e03bb89e07e1a5", + "0xaa377edb22f3b6832c85b39443e2addce35609872bf62447d5b482d94dc69eff", + "0xcdc7c986bef6799a7b7aed7a38307c79b834bfb880319aecc678d4f1a76f3473", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 62, + "address": "0xa186727fdaf90cd7d9972572e32c618ce04206f8", + "amount": "12855384000000000000", + "nodeHash": "0x547b3791b7b54d8fd2dd80a3ee836ecc231ee707bfc7cb6083933fe92d7b23e0", + "proof": [ + "0x5360a65b13a4457735f913e59059a7bbc5aa42175bf4084e8bd488b2094c7f2e", + "0x4820465c864e10629d6541621ab13ba0910dbd01c40270687fc205791d3b9a38", + "0x4d72bdbb948be8a9d3a660d43eaefb5cccc88ede0e152cd8fed46d934ad5d0b2", + "0x1d51ca0e19aa543a6545b29cab8ac5f9483bb15f0c024a81b9aadfeb29b9f54f", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 63, + "address": "0xab2e11c99d8830d5d9b2494a565c974595e39eef", + "amount": "13988133000000000000", + "nodeHash": "0xf92a1f17d43fe8171e77be455711461ae865e1c2b52d37c06ad9dda67fbad1be", + "proof": [ + "0xf8d5385c20078d9a9a66fa4e1b5a1033963d5e828f267a6aad9853ff77643726", + "0x1b3912228974000a20cd4cc195e0c6f83a56d09367d32d440ad027a03338a850", + "0x16dcfeef35a8dc7ec586c8ce9af48cbdf90a8fae3fc96ce8f613094b4835f4d4", + "0x878b93f5379152f0d671b155b7a722b05eb555558bbfe56bfbb394a485c7952e", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 64, + "address": "0xefd0199657b444856e3259ed8e3c39ee43cf51dc", + "amount": "8087301000000000000", + "nodeHash": "0x5bf6873605819caf83ac4e9c81b51791ff9eaf6ab2ea3cdc7ec0a3bc8b7d16aa", + "proof": [ + "0x5e81a86e87207aa13fd8e6f14826589827fdfb0bb5f2e1f4efb5ed66f925456e", + "0x353af0e7766713cf8d7c6484ec134f5393a59686809d983c15e5ea91bb973523", + "0x49caa557cb6111a0eda5b1f13ecfa94fb09fa036e054a1faeac9f3ba92b8af6b", + "0xf97c70e9a61a1af1e7abe2d3dc3538c1ed7c5556d96b05a481e88adc38bbb1c3", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 65, + "address": "0x87dd56068af560b0d8472c4ef41cb902fcbf5ebe", + "amount": "7753623000000000000", + "nodeHash": "0x15b1f25d8222a0a790d4e9f8155385ceb11f665aa9539eca7b47b2d7043d9448", + "proof": [ + "0x153b5d57c32a01c9ebea820468594d6d18d98eaab99af1950c5c65068c59185d", + "0x6c294dd472dfe45c9fc8ef3b2f6e24a9d5cafdbdcb1124da7041c9955bb3c750", + "0x0e31324d8f7bdec33bc699b1ac845b07055f9836b7852f51934cbaca50f4967f", + "0x401b4840cfd7296b17a0bb231a8ee94c8c8fcd710eddb6978d46e0faf47d8eef", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 66, + "address": "0x476e23bfc5415397021a74e525640b328a12b81e", + "amount": "7393602000000000000", + "nodeHash": "0x630c4295cde02fdb9d6260ad37a0d29d1803b62142c649a9886e8496fc2c2be6", + "proof": [ + "0x6249b701f7da9ed7dffb8e403b373121968255ecb55204bd1ab053d55c18713d", + "0x47b1c49d209c6a87ea4868cd3a25a868412622dadf3a4abfb52b4911cb51cfde", + "0x7ec8f9e5ba1f89b9c48f931eb68bbea8c0cf12082f3f60cffe31bc43916aed11", + "0xf97c70e9a61a1af1e7abe2d3dc3538c1ed7c5556d96b05a481e88adc38bbb1c3", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 67, + "address": "0x0a74f5262394adec557a6bf7b61d771da01cea59", + "amount": "7174077000000000000", + "nodeHash": "0x8063fcae6ea90f3b8621b4d955a07e5d9f1b0c1723b8f35450807ca81d653947", + "proof": [ + "0x815feb86ee20a6d008778e6ffa6382d189cd7c1b3f60bf5bc30deafae4f44f0d", + "0x345c7478f3f9dbe03e37b5482bda9e03fa6246419c4ddc4cd780b55992dbfd3d", + "0x5328a6b8be51d45cf7e2d0f283625bf5829d066050211d2e89db5dc907c1305d", + "0xe92595020625bd816794a0573140fc5d9e5dda15bf5439f01004d872a3811f20", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 68, + "address": "0x2829419f7cde7054237ca8610ce29e39bcb0af30", + "amount": "7059924000000000000", + "nodeHash": "0x0d5a5081de2e78e5032898bfbcb3dd94739d34ef4f644c3b75bfa3ad5f173262", + "proof": [ + "0x0c47dc7d3bb60a1e01119485736402bdb29815cd508670cd1c0eb3e6bd0949b6", + "0xb963de4f4436b346233faa43b8ae655c28e903aec74775f8954e845d8a729412", + "0xc7e6a378be7722288c6d0e53e9c3c2681b948dc95aaa85fc9e61e4385b30260f", + "0x99927aaba68fa1c526c8be885fed2a583000a016f39e6210564017b20cc96842", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 69, + "address": "0x4fc5a19e45de02592a4d0241869f969f68255e9c", + "amount": "7850214000000000000", + "nodeHash": "0x30572f5c30ab4086c014cfc33461371e2ce8cad8e3cc9e0a71781be74d28b65e", + "proof": [ + "0x31107d68b1e228215b3fa333c9d48984427de785a99d56f04640a6542b236382", + "0xdb43f9b35e0d6b2d855e60ca6191e14184a51d855bcc0e057de1ba8b1be550dd", + "0xae70dc4f1661eb4cd5ce6ce92d37555b22131bab4d1e2299a12a96bb693592b8", + "0x3f9c8e77affbca217664af93969c37b75db5f5e970d56dd0a2f7b374a9d8c12b", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 70, + "address": "0x4ad087aa85980ac2ad2659193dbece1d1308e0c0", + "amount": "6928209000000000000", + "nodeHash": "0xa3b291cde829f82c45a4b1d4e038ea92bbc704beaa5d987d0688d946c82ddcd8", + "proof": [ + "0xa500d5b5bc87cd16db273611b103367bc3e78f272b4521a3e344eca724a39e3b", + "0x16e69caacd8ffd43e2913bfa0c48e6ef0b745d7912fc67043ff2afa1aa2a8937", + "0x2a04f687325c7538cb6f084fc767c5ed72dac0dad6a28ef23986fbcea61df2e0", + "0x112c5e47475d52c67bc98d675e1056afe3e6623117b94507e29be69ebf20a435", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 71, + "address": "0xaaf2be02fbcf9837361cad6bd08dbbb139395140", + "amount": "6901866000000000000", + "nodeHash": "0xb80487c07af4b3e1e40fd67b513cd4dbd21b7964639b308edf5ebbd8d9f99384", + "proof": [ + "0xb5947c5c069ce6bc94929167d4e0e254f167ed9d0e115dd8f7747b27b1c02582", + "0x7c7373ea5af83cdcb86eb83b2d2c45f847f5c805bfd7adaf7c31435ac6c016c7", + "0x28f06e02ae1339fc54b9c964d1b5ad118e18997a61fcf36511d31cdf927cc508", + "0xc9285e31e87d3c235fc0dc4ab5c5978891a414a32a26c69b43d5834adf7fea91", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 72, + "address": "0xb21731d830b7e8460e5d6c66dc1ccc0a5d0a6b87", + "amount": "6814056000000000000", + "nodeHash": "0xe3fad270fad7dcfdc379b8a015d69d93b57af9c4d604ebe8ffd8f9392ae1783d", + "proof": [ + "0xe4c9ba7f0beef2d8806d952216d1aa7eab18a6212c49edbe49e35bc3633c2dee", + "0x81e9bfc06a226d2b097763c0de33d2076e058f4bc5a20cab44c9a56f1cd18709", + "0xa88906f3d7535eaeabc785ccf9848d827c1660f4360f3f002aae67aa7a82fb27", + "0x2b7b07c087af29ef8f3779ca825e620e6a91fc1b8119a477cb9747b5bfcbe5c0", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 73, + "address": "0xc3a1b0600ba577587b6f9441abf5baa9f0804819", + "amount": "11775321000000000000", + "nodeHash": "0x8195ff1d91fca20ad1a7d2dd75d0196da290593306ccca624270a030a95f8382", + "proof": [ + "0x82c5a7b709a7d5b49d7420925c9c98025cb4a7345edcfa5819e9bc6b001eb6a6", + "0xb2095b3b5602ca26b450a6c509aff07f2b29d60fa605c1ee0f574eb502fc5715", + "0x9ee727d3ed6f58402a106625a4d4fd294c3e9046de7e06e40540f5ca498d0432", + "0xde8a996044401b7617b89b4add1c4cfe27060c1b474a658abc3cbe0c0ca542cf", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 74, + "address": "0xf3d9281fa183b74f32b96e1c5244596045f4ede8", + "amount": "6304758000000000000", + "nodeHash": "0x2d7351989fcbbddf16a097cdb50a5c6f93cb0c628f604747e43997b317c55058", + "proof": [ + "0x2cf1ba96f731c675f65cbebd0b4587317aae9f0dd18567a0b9e7937e0c436d9f", + "0x46dd98f4ac64a8db6b23b9cc113feb6734b218225124da325287b8b692b73e8b", + "0x69dffad5ef567bc4c7cbd4fb5e9f41e131955b2a032a5ab4e7e97c5225575d1c", + "0x3f9c8e77affbca217664af93969c37b75db5f5e970d56dd0a2f7b374a9d8c12b", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 75, + "address": "0xab58779cec2b82a75ffd103fdc88d7e3adb13468", + "amount": "6287196000000000000", + "nodeHash": "0x40db9a71a09bf9910715b28b63151913bf8a449af935d03274a89f05e8a23ff1", + "proof": [ + "0x3e843369391fb3b12d59236c728c16848eac833c901b432d1fc03f3f77841815", + "0x70d27396bb6ec60367db39c731dcddf4b7ec5583fd51a5ca1ac054a5dec1cb79", + "0xffa19acfd6a5edef0a1398badcdb78ec21db652ae2404d2ebbd5b99401dc1c0e", + "0xbd97d3672cdd0dce515b28377cdb7ddfc5d08d2a126f287bf0f31ad87296aab6", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 76, + "address": "0xbc6f1ffd1fc6aa7fa2b29d20d9b84edc8887aa9f", + "amount": "14286687000000000000", + "nodeHash": "0x4f1f84e694e849d9208a4217979ee0e2d7f41d55b704960b3801d138ba83a6dd", + "proof": [ + "0x4fe684cb933bca6ebce70a8a5d2805128eaa0681a931cbb31357a08b139c5d86", + "0x2a658f536b92a8ae35d0beb6befbce9b91af86b348291522a1bff519ae67799d", + "0x757b5f89f8f0c01453b56baff0bf23c0613755707e6c33d7c6a462624ede42c3", + "0x1d51ca0e19aa543a6545b29cab8ac5f9483bb15f0c024a81b9aadfeb29b9f54f", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 77, + "address": "0x37b1884205cc47c2bf5cf5a0248946c4c0c85724", + "amount": "6155481000000000000", + "nodeHash": "0x53246a05309962591b4b538c57aa4e4b1b6889d37ed4d9e9a67c30ea9673e46e", + "proof": [ + "0x50f087d27be667e26602cbfba7e50ad22f33ac898610f10192a9da5118f0e95c", + "0x21e618612ffca0a2548757633189cbcde9cc6b1278db931633e13d1d8889b27c", + "0x757b5f89f8f0c01453b56baff0bf23c0613755707e6c33d7c6a462624ede42c3", + "0x1d51ca0e19aa543a6545b29cab8ac5f9483bb15f0c024a81b9aadfeb29b9f54f", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 78, + "address": "0x1aba978cf66d4bd8ce54ab5e3aaff177ab901fb6", + "amount": "6129138000000000000", + "nodeHash": "0x9dc8b99fe2e2d355ac61fc081db3139fb08514e518042cb8a8b99a2e7e16a287", + "proof": [ + "0x9d69517f0bdac5765aa462977f1daa57f12b2c951662f10f7caaad817798d0f1", + "0x05232f0893f1accd6d9dff4e6b2344dafa4d5909c16b85302885553a492fdc60", + "0xcfb31d36e8200eb045fe23af2958fcf64e7c29d370930d2f986db9135dba31ab", + "0xfc494fd4d56122448384ef08f6ebb78947a312c2e1369042b28819274e2acf66", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 79, + "address": "0xb1ec8d9e3c9640d9cf9a677066d0142668f11cda", + "amount": "7314573000000000000", + "nodeHash": "0x5e81a86e87207aa13fd8e6f14826589827fdfb0bb5f2e1f4efb5ed66f925456e", + "proof": [ + "0x5bf6873605819caf83ac4e9c81b51791ff9eaf6ab2ea3cdc7ec0a3bc8b7d16aa", + "0x353af0e7766713cf8d7c6484ec134f5393a59686809d983c15e5ea91bb973523", + "0x49caa557cb6111a0eda5b1f13ecfa94fb09fa036e054a1faeac9f3ba92b8af6b", + "0xf97c70e9a61a1af1e7abe2d3dc3538c1ed7c5556d96b05a481e88adc38bbb1c3", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 80, + "address": "0xfaa549ecc9500e5a197fff0c2833166edc6142c4", + "amount": "37872453000000000000", + "nodeHash": "0x8f4998435932efef621b831d52e82a15081bea2be441c972e29d8c8692dccc12", + "proof": [ + "0x8f66c59eccf7461e7efc0bd6840a7259af1683280149a757655e2e8e7e8ebf9e", + "0x42d66b4f5f6c05361d84ca6c8b2d790d21c1185a469b9aafbbc37c9533a5ea50", + "0x70320b540506f9453fa1e4f3dcb5f67f9cf8ea4c194e04076144154651c71b7c", + "0xcdc7c986bef6799a7b7aed7a38307c79b834bfb880319aecc678d4f1a76f3473", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 81, + "address": "0x9af7717db595bc4c513b19cf1c14af7e00345fcd", + "amount": "5391534000000000000", + "nodeHash": "0xe8c0611d5a166d83fb7daec5338d4bda952e97e744f02dfaf7d34daebf4c3917", + "proof": [ + "0xe9428be0a2ba85e20788c5cf37a24bc91f320e6d2e56a38cd9eac66432739ed9", + "0xf857ecb5b3daccfa04d13f43c273f976827b19767b0957dc7958d9f94ef23202", + "0xbf8af7b1dbe7d5a234d0212ab5762422bb4c90ff91e57449a0ed92527cd64ad0", + "0x3f59b8cdb34f4bc3b9c931471fa7d84b69826722d8401478e0c9afbc1f56fa16", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 82, + "address": "0x769a1ef6fbfa918d685ba02f3c2e45cfb902ebf7", + "amount": "7042362000000000000", + "nodeHash": "0x6249b701f7da9ed7dffb8e403b373121968255ecb55204bd1ab053d55c18713d", + "proof": [ + "0x630c4295cde02fdb9d6260ad37a0d29d1803b62142c649a9886e8496fc2c2be6", + "0x47b1c49d209c6a87ea4868cd3a25a868412622dadf3a4abfb52b4911cb51cfde", + "0x7ec8f9e5ba1f89b9c48f931eb68bbea8c0cf12082f3f60cffe31bc43916aed11", + "0xf97c70e9a61a1af1e7abe2d3dc3538c1ed7c5556d96b05a481e88adc38bbb1c3", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 83, + "address": "0x601b4927a0e370464488b306a636dce604d6f577", + "amount": "4926141000000000000", + "nodeHash": "0x322c42f26d9bc1a993a977fb9c13de76b8c6a9e3bad315c2f3739e4f23e0a80a", + "proof": [ + "0x35e3a4fb47d502a455448dd9db9412ba51331b1c8027e51cab9735ac6584cbb5", + "0xdbc5b17868611035ed7b5fa15c9c514cb16c62fe167523d952200d0694290ae5", + "0xf7478ef32da8f4830df5edcd72303756fd6f171973be2e49aea5212970a2273a", + "0xb149db133f2de53fa1c5bfb7817947aa4b91fba91a7f427e3cebc44a51c0bb8a", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 84, + "address": "0xc51fa42503942cafa7b1ffc02f0cd9564189773e", + "amount": "4855893000000000000", + "nodeHash": "0x2cf1ba96f731c675f65cbebd0b4587317aae9f0dd18567a0b9e7937e0c436d9f", + "proof": [ + "0x2d7351989fcbbddf16a097cdb50a5c6f93cb0c628f604747e43997b317c55058", + "0x46dd98f4ac64a8db6b23b9cc113feb6734b218225124da325287b8b692b73e8b", + "0x69dffad5ef567bc4c7cbd4fb5e9f41e131955b2a032a5ab4e7e97c5225575d1c", + "0x3f9c8e77affbca217664af93969c37b75db5f5e970d56dd0a2f7b374a9d8c12b", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 85, + "address": "0x963eb0638a2fe075269b4e83f886e89d855ea4a8", + "amount": "4495872000000000000", + "nodeHash": "0x60310e3189b01a8eb478998f052ed397c87e84a8e898ef76430dd8e238a128a8", + "proof": [ + "0x61236644a38b84cf5e9af0de1272bc43e1d41e1c1b8f686c4d6e8c083dd0181c", + "0x3dce93286ba0d83852280a31a35584e2d00e320703d859d82d14d4d769e2f44b", + "0x7ec8f9e5ba1f89b9c48f931eb68bbea8c0cf12082f3f60cffe31bc43916aed11", + "0xf97c70e9a61a1af1e7abe2d3dc3538c1ed7c5556d96b05a481e88adc38bbb1c3", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 86, + "address": "0xa38475ec707f358f338fc2dc393d92845276e568", + "amount": "4469529000000000000", + "nodeHash": "0xe626355269371632b18e02d63b674ecf90ca397d832a86e13f04e387136d6837", + "proof": [ + "0xe50c0a6866dfc4fbac9ebe9509ca06c14f511864e34a130fbe9788a66925b34a", + "0xb7543e976bc75d1fdb35f7e3375de2d8f4d636569988089f030c07088a21544b", + "0xbf8af7b1dbe7d5a234d0212ab5762422bb4c90ff91e57449a0ed92527cd64ad0", + "0x3f59b8cdb34f4bc3b9c931471fa7d84b69826722d8401478e0c9afbc1f56fa16", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 87, + "address": "0x97b3ec0c3ece7bdce5a857ce26768dfd880c14ea", + "amount": "11424081000000000000", + "nodeHash": "0x444a8991af4dd46166269c36b09067fffec991ec9542f68ca10c6d029c486e0a", + "proof": [ + "0x44970fa0ec9e88801114883c8a934c4868698ee159c77e3db81aa8bf47e02a54", + "0xd9c25e45e318bee712d957043e4a6a8c3590baea7debeb06b8bd2ee9c638ee1b", + "0x69213dfbff5f3d0347b6bc4ea800818dd36cb94d6fa797a3ceb3dbac048e242c", + "0xf5206d1a8eea30d8766d8236039d92db78f10d58c4069671da4386cc649fb9cb", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 88, + "address": "0x43b0d17cad8a44ab824823f8ffd12cbe36d59728", + "amount": "4223661000000000000", + "nodeHash": "0xaa92cdf5077e53958697a5f1572b5c9b1cd9f69253605b8c235b06ffd80c193d", + "proof": [ + "0xaa9f98a1e3ede0d6e2cd08c460f8db43a36e1e44eda1bfa19ef220e694cfef31", + "0x7f9f9dcdc834eaa9c7e314a7c350494e47149a37ec2dad4a244515d2da47033c", + "0xce2d66202d26fe1faf88499baedd97a1f2f5dd90211b19a3d4f77e9b88291311", + "0x77aac907780e1eb7ed08bc6fb93c7a472125d7cd0e8720e5b60c436f725d4b01", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 89, + "address": "0xd4e8ad52e6027ec0c6812dd175f1ecf196fb61ac", + "amount": "4144632000000000000", + "nodeHash": "0x8be98a9a35585e8dc57f4968eee19ad7ee0e42a7c4c2ea82d84866b2a56e4326", + "proof": [ + "0x8c84f088e53c994ed245357214efdb8acb0c742fbd68061018fef6a62de3aa8a", + "0xea4ff5fcc7c0fdb07adc0c92de6c0a2dc4d3c9c189b38b43f4f9f1d8c7910c1a", + "0x70320b540506f9453fa1e4f3dcb5f67f9cf8ea4c194e04076144154651c71b7c", + "0xcdc7c986bef6799a7b7aed7a38307c79b834bfb880319aecc678d4f1a76f3473", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 90, + "address": "0x576b1c2d113c634d5849181442aec5a3a9148c1e", + "amount": "3995355000000000000", + "nodeHash": "0x2054ec96506440bef9e73da667d94d714e17f667796e533d94b08ec1593ea89b", + "proof": [ + "0x1ebcfb87f058e147f7af10b30dff3aba727adbf49a1988697c80c3c3d214256d", + "0x99c6f8f0df2e40cd0d8419b827cf8c1dde54e98732e7d6e043e79368f96f1ad9", + "0xbce9650f2ce0bdddf4d157210724cbdd8249457433191a81be341fcaf6c64a30", + "0x1d9b0c09a3cb63729974fa7589d3e021eba7ef36ed2ec96be004ae76e40e4456", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 91, + "address": "0x66bb7c182672a4b4a1c5891b9fd708b241ef3622", + "amount": "3916326000000000000", + "nodeHash": "0x4ba85cfc345b6efcd2457dcd6719f9a59627351a217ca4068ea95d24735029f7", + "proof": [ + "0x4c9f39fe6d842887da96928f2a1b087800c0de3e0429032c22b15ca3295c33fb", + "0x76f40ebe9f7ef73c61c2a6c90e1bb3a013223f86882771c6c18a4e59e395adaf", + "0x628f4d9731d41648d4ca18d657beca29b7b92de3ff4b8838cbee00e38ccc4ad0", + "0xf5206d1a8eea30d8766d8236039d92db78f10d58c4069671da4386cc649fb9cb", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 92, + "address": "0x50bbf827267c4e920b512f8aa6c1c9d1a465a128", + "amount": "6559407000000000000", + "nodeHash": "0xa30f985f84eb0b752c29e18cdbd527a5cd9b0ae58ec33e97b60103c13bcfde80", + "proof": [ + "0xa2b407407882c46166ecdb12eea3eadb3420b4854c43d4fa116d201a92ba8e88", + "0xe03539dfb7faeff1c7195d7af6d45594dd78bbd31c8bc23678ea9dbd8d2cbd92", + "0x2a04f687325c7538cb6f084fc767c5ed72dac0dad6a28ef23986fbcea61df2e0", + "0x112c5e47475d52c67bc98d675e1056afe3e6623117b94507e29be69ebf20a435", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 93, + "address": "0xfb0f37c0456d60eded9b64442dfb0c4847cdd06a", + "amount": "3723144000000000000", + "nodeHash": "0x10179f43094577201f5ddcff444dc58365ca5dea79ab3c3ed50612be1ba5743a", + "proof": [ + "0x108f41c128657d67ec76fd03d7288062f15ce05331cccc1cadad73ac1afcc928", + "0x909a8ced8bd678cc4ab8a9091b213754d3241812ff8b44cbf13e34e7c1aae49b", + "0x1852df03d73c3d1dd34606bdf92d30c73d72be657b7e791005521cc89c201bd4", + "0x3568ae0dd2327f26f3bd4c1af82d9a7d8132e50855cdc1c20f276776264d6ab5", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 94, + "address": "0x0fc84e15aeb7907a76a221169d80d3eb06cdb44b", + "amount": "3573867000000000000", + "nodeHash": "0x8904c226484fc9bcd5cf04c8836b57cdabe792f22adafb2675887f3368425532", + "proof": [ + "0x8b361f814a7b3d5f57028f091b9a89b596243810d14e3c7435f6a14885e24ff6", + "0x4c4fbc32f05440226a7cdb7a0f22e1863eed37c19212fd9becda0b41c4527888", + "0x3697a1b81c3b046b0a765a73489c36042e120349ca90156ef89e0003c12bfefe", + "0xde8a996044401b7617b89b4add1c4cfe27060c1b474a658abc3cbe0c0ca542cf", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 95, + "address": "0xd6b2095e913695dd10c071cc2f20247e921efb8e", + "amount": "3450933000000000000", + "nodeHash": "0xd12e0f00e9d13194cc4d6e0b2734e73c4771eeafdef8df295b5c736f8cf93ccb", + "proof": [ + "0xcf17c5c81df8c624386c8d1a3bcf56a80e7246b66c2b79220366bdbce9f0dbaa", + "0xe110e1b093e14e02b1806da636ee9a81490693e411fd80e7e49a4b2dc8130b5f", + "0x9b6bff949aa19fffa82a45966c6d082c7dd2c952298b7ab43268d0cfd3411865", + "0x049057c435e918361a347a9fe31a6f48819b657ce283c5390310bbc944753eca", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 96, + "address": "0x0aa350fb487f75d4f5d6ed920a5ae0923ded06e1", + "amount": "3933888000000000000", + "nodeHash": "0xaa9f98a1e3ede0d6e2cd08c460f8db43a36e1e44eda1bfa19ef220e694cfef31", + "proof": [ + "0xaa92cdf5077e53958697a5f1572b5c9b1cd9f69253605b8c235b06ffd80c193d", + "0x7f9f9dcdc834eaa9c7e314a7c350494e47149a37ec2dad4a244515d2da47033c", + "0xce2d66202d26fe1faf88499baedd97a1f2f5dd90211b19a3d4f77e9b88291311", + "0x77aac907780e1eb7ed08bc6fb93c7a472125d7cd0e8720e5b60c436f725d4b01", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 97, + "address": "0x81248e53c2aea91dc28174df9f9bbdc3ac76b534", + "amount": "3248970000000000000", + "nodeHash": "0x57d3b4419edd2ff472c8073e8bf675f7976fb491175d51406065d1e8fa7a323a", + "proof": [ + "0x56054f809abf6ae70fbdd71ac235716a0e01195f877c18bce681ef67ff9231a3", + "0x2ce401ea4f624752dc4f2611b2af6ed78386103d3e9b42a04648e679492f0f3b", + "0xa29dab6c266c58115bc9610428c6698333af363ab59f695850636c30b506a079", + "0x58353663046a578dd1f9997dc38a4b7831c0a0b3796a5c26135ad8e0b4324c80", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 98, + "address": "0xbc88c55a7f80899d91d98faf0cabbc3fa6df0fea", + "amount": "3152379000000000000", + "nodeHash": "0x66096b750d267fe5a42a20291ca034c628fc5e435c184ac09c61f2f9fabcac6e", + "proof": [ + "0x65c1cefeb0f49053ade79a75fb7267fa57a9b3e40dc0fe58ae67fcfed31ead0a", + "0x43ea64ea828c2a81f4a0dee2f0a601b72bcb2d4d276b13489a1a35da50168959", + "0xd2aade99c8f850b3a3bbe9cf35336218f89132a869dd4c4641d5e9f5bfee5f9a", + "0xbef3adfc06e9c1b99f0036e6bd56baa2f939d20e0f53ee8ccdaf789586255173", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 99, + "address": "0x14ffae1d2804eaf261586a6378218eb5e7235066", + "amount": "3090912000000000000", + "nodeHash": "0x851ab05a225a1373e0b64d9e5baa033c7786b7408a9a833c81881f495bff132c", + "proof": [ + "0x85ae1e64245e119c57c096d3e3e379c677135d7e25491a94c4472e3ea8d735e9", + "0xacc504eebf4d3892781c5ffbfd444dcc9b41e288f1c11178a72718e1e50fb2d5", + "0x3697a1b81c3b046b0a765a73489c36042e120349ca90156ef89e0003c12bfefe", + "0xde8a996044401b7617b89b4add1c4cfe27060c1b474a658abc3cbe0c0ca542cf", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 100, + "address": "0x9e0bba8e7e9194476f1b3d329ae7f988f580da3a", + "amount": "3055788000000000000", + "nodeHash": "0x4796a5c8ffefcaa6037117160662afa92c8cabe99db4f9bfeede846860a35fed", + "proof": [ + "0x45cb3d77485ab5b160d42585569b47dfcbccc969cbd0a767141b5ae8b3a9e939", + "0x87878a52868259f7af40a9350822b0c16cce4921d6b16db57a1de439bb28e528", + "0x69213dfbff5f3d0347b6bc4ea800818dd36cb94d6fa797a3ceb3dbac048e242c", + "0xf5206d1a8eea30d8766d8236039d92db78f10d58c4069671da4386cc649fb9cb", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 101, + "address": "0x260cc6b39a3f2a852ce65029e02db46d0f6e1582", + "amount": "2836263000000000000", + "nodeHash": "0x3da9db8a3b90132c99daeed68998cac9d4dea3455106a52a0842de8c02757d63", + "proof": [ + "0x3d61ec10ac21b868020d4329badabb9020aeef6c583b4928bbbec5a1972dcbff", + "0xd53c77b964acbf4670c9fc29923f25a1e18bac9e66cb4b3eadab46131ea12a61", + "0xffa19acfd6a5edef0a1398badcdb78ec21db652ae2404d2ebbd5b99401dc1c0e", + "0xbd97d3672cdd0dce515b28377cdb7ddfc5d08d2a126f287bf0f31ad87296aab6", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 102, + "address": "0x40d80168b6663700b6ae55d71a8c2cf61d0c1225", + "amount": "2704548000000000000", + "nodeHash": "0x1e997a55b05bdab2a284a5e65c9911be652bf6df160059172c683585f8790fed", + "proof": [ + "0x1c7695ef4b33446b2866c11c50374e3ed79ba3488bda0ae639592ec43119bd67", + "0x25ee7413e5ff15c1f681c4be625cdc805d06ce01ceea396542a6bbcee9deac17", + "0x52f131eb5f23b186983cc1103bdf44f5286b58cc5da6da8de3fa399de2a0459f", + "0x401b4840cfd7296b17a0bb231a8ee94c8c8fcd710eddb6978d46e0faf47d8eef", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 103, + "address": "0xa00c99f5f955552742f1089ded88abdc74e67bac", + "amount": "2528928000000000000", + "nodeHash": "0x3e843369391fb3b12d59236c728c16848eac833c901b432d1fc03f3f77841815", + "proof": [ + "0x40db9a71a09bf9910715b28b63151913bf8a449af935d03274a89f05e8a23ff1", + "0x70d27396bb6ec60367db39c731dcddf4b7ec5583fd51a5ca1ac054a5dec1cb79", + "0xffa19acfd6a5edef0a1398badcdb78ec21db652ae2404d2ebbd5b99401dc1c0e", + "0xbd97d3672cdd0dce515b28377cdb7ddfc5d08d2a126f287bf0f31ad87296aab6", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 104, + "address": "0x7fae72a3954b1d5ab585cee84b34975326eb5a98", + "amount": "2256717000000000000", + "nodeHash": "0x09ba9a82855bff1513b46a6063e38af605f1c36d53df3674058de45b81d0543d", + "proof": [ + "0x07087e3ca81520beee67237b32b53372f88d40db003d9ef3ae043bb80dcfb99e", + "0xb788935e7bb4bacecd7d56f61f8db99689026fb698ec7870dfe58fd4fd61cdc3", + "0x2ce668e3cdce60660255a67e7d4a5edf5412f5f39d06875e56a07a4c1d45097d", + "0x99927aaba68fa1c526c8be885fed2a583000a016f39e6210564017b20cc96842", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 105, + "address": "0x902ba52dcf1280fc6bc101e71d9158818018fcf9", + "amount": "2168907000000000000", + "nodeHash": "0x108f41c128657d67ec76fd03d7288062f15ce05331cccc1cadad73ac1afcc928", + "proof": [ + "0x10179f43094577201f5ddcff444dc58365ca5dea79ab3c3ed50612be1ba5743a", + "0x909a8ced8bd678cc4ab8a9091b213754d3241812ff8b44cbf13e34e7c1aae49b", + "0x1852df03d73c3d1dd34606bdf92d30c73d72be657b7e791005521cc89c201bd4", + "0x3568ae0dd2327f26f3bd4c1af82d9a7d8132e50855cdc1c20f276776264d6ab5", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 106, + "address": "0xc1133c83d409724727ff6699f14f040746e5ad01", + "amount": "2142564000000000000", + "nodeHash": "0x114c90054a873891468d67e2c1ad6a54ff280056e11588a87ef498a0c22a4c75", + "proof": [ + "0x12f74f4f36ca93cd83f83fcd5d38c6a5a7b0c75d8c4880bcc9be7be2b42c8601", + "0x63c2f004a196c062b79f867af6c4c91a851891347e2e60d885598c9f7fa54bad", + "0x1852df03d73c3d1dd34606bdf92d30c73d72be657b7e791005521cc89c201bd4", + "0x3568ae0dd2327f26f3bd4c1af82d9a7d8132e50855cdc1c20f276776264d6ab5", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 107, + "address": "0xbbf3e43681886f30835eb3ce8ec210b8ff16e85d", + "amount": "6682341000000000000", + "nodeHash": "0xcc64d94c8158ac3c856582e86dbcf00b29461ba77121921cef3d807a0a8e2bf7", + "proof": [ + "0xcc9c49a5b1f7ff9f554707e876d986eb96de2cbae4d45b8e77467d2f1bd35a06", + "0x626b7860b684e172c34f58fba6d1f7fe69b87c6cc98d88a376adf9537e3d131a", + "0xba770e25357a76c9190bdccfaf3cfbc9f10ba8bcf8777d82c64997ab022c0e3d", + "0x049057c435e918361a347a9fe31a6f48819b657ce283c5390310bbc944753eca", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 108, + "address": "0x6be0b696cecfd25ab1755d155b323c1fb590a196", + "amount": "2089878000000000000", + "nodeHash": "0x6af043512246e059286f5c7298e40fe58946671677a6fc4cf96a9815fdcb6583", + "proof": [ + "0x6a0227afb109fd35bc100d93a978262714b40d7e1285c99755b761ed2a807a89", + "0x0d14719848a014eaa8f0c734eafd7d13dcefb69907a041a7469e38037977c861", + "0x428c48ad33d483236e7b022672c662d2cb14b43b2cc565e3cf513754d2973895", + "0xcd656b995bfbb006bd6cc7e0553e7b43a8a428952ce7e40550495ee52cd3d9f7", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 109, + "address": "0x6e0bff79137309bd156123ea6b9f24c353312adb", + "amount": "2089878000000000000", + "nodeHash": "0x5a9c9df89a392afc1506f578746e2a07fe8d9ed57bef284c3a03ca66095f62dd", + "proof": [ + "0x5a598f41d51165d456db10444293183dc37ad6b82ec7ebdae2185f0d215b51af", + "0x8ad619875948d9ec90624e097d678c60d61a4d105ad7119c54ff600e7582a53a", + "0xfb6b61993f883176c7796e3c8f60ed9e153ddf14ee510fb2e75e88c610562b34", + "0x58353663046a578dd1f9997dc38a4b7831c0a0b3796a5c26135ad8e0b4324c80", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 110, + "address": "0x48888db73eb818dec38fed2c8d4d08252177de0d", + "amount": "1940601000000000000", + "nodeHash": "0x2a6c8939f84daf5408b386090794d0c9255a6fb47616e5df7eb8295746808075", + "proof": [ + "0x2ac56981bea8ffe5d21014b5c9e8a9e2cfa92a125d2205c9bc5e9b4cc958113d", + "0xd11a8de1fe3f2ab1e946fd84c0c6b26e8c263e7e6e6825020cd776a0a8a7b944", + "0x8b17088d53d8cd82f15cd19fd878ceb428963b30615a65fbe1afc21c9fac39d5", + "0x1d9b0c09a3cb63729974fa7589d3e021eba7ef36ed2ec96be004ae76e40e4456", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 111, + "address": "0x9c330a97c3dd093f4b514af6cc2f531ac0cb084b", + "amount": "1870353000000000000", + "nodeHash": "0x3d61ec10ac21b868020d4329badabb9020aeef6c583b4928bbbec5a1972dcbff", + "proof": [ + "0x3da9db8a3b90132c99daeed68998cac9d4dea3455106a52a0842de8c02757d63", + "0xd53c77b964acbf4670c9fc29923f25a1e18bac9e66cb4b3eadab46131ea12a61", + "0xffa19acfd6a5edef0a1398badcdb78ec21db652ae2404d2ebbd5b99401dc1c0e", + "0xbd97d3672cdd0dce515b28377cdb7ddfc5d08d2a126f287bf0f31ad87296aab6", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 112, + "address": "0xa8ff20f8825c758c8404b9edbee354e7c4231cd8", + "amount": "1817667000000000000", + "nodeHash": "0xf4a1752599644a28a1c5dc60ce7ddaeb0601488eaf5941f2e519eeafaa921daa", + "proof": [ + "0xf50625ae792acc8fb86b2ad484754f9199c736b94e005962fcd723c2fea10210", + "0xb344c8d5f1591e09ba72c6e9eb4cac6f5f0eb64224b0cb3fd7aca8321278d7bb", + "0xee9e134d432662feaf476361874386ca78329b4992de686bf58e678134ebeb7e", + "0x4374227d00e14e26a6659c58e26ba194704a9c4428299eefc28bfb5fbdf9f12a", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 113, + "address": "0x8017ad03e9fb98d42236faaff48c43109371dbf3", + "amount": "1536675000000000000", + "nodeHash": "0x0557dfc6f86bfaef544d914e5bb9f82588c6fb84a4728f0ca4813de3427f7c69", + "proof": [ + "0x05c54c057df3df589f44d0434cb87b8d856f72b93afa90f10c44f255066b8278", + "0xd4293db5634e17e9208228d938b5ac8030969823042f29bb41c16ab47ef9da11", + "0x2ce668e3cdce60660255a67e7d4a5edf5412f5f39d06875e56a07a4c1d45097d", + "0x99927aaba68fa1c526c8be885fed2a583000a016f39e6210564017b20cc96842", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 114, + "address": "0xd3802dce7f7a54d59e7a64329d287624ff3f495a", + "amount": "1431303000000000000", + "nodeHash": "0x746e3770d2746748a307245379404830d7b388f6f050b9f2f851aa74d380ac8e", + "proof": [ + "0x750ce3cfcc3c375c355366b818b450d9e937aa30a2d9789cc89ce05cfeaba647", + "0x97165447885600a06e18a2da7ed0112cb0164976a9bd0208beaa8773b89f5079", + "0x27e457d39207c5c9413452f66d1439674315c7ac031965edc3ef5983d37b3479", + "0xcd656b995bfbb006bd6cc7e0553e7b43a8a428952ce7e40550495ee52cd3d9f7", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 115, + "address": "0x8fe9aa966b726895a95353b56b4f3d2b34046b80", + "amount": "1317150000000000000", + "nodeHash": "0x99b5ccafc49e01f50adb44624c8d6fdd007b117d1a0cac56a81e85c0061f3a1c", + "proof": [ + "0x9adae8c124ab6de7ff841311465d2ff84c248325051f5ef34602c32136e918b7", + "0x76c5f38de78bdd5e40676bc0aef50d302398699cc45fd2ca3620abcb86720d6f", + "0x80d3320efcf13447783dc7c21219a96e1b9acfb649c33c72903af272ff3b6f25", + "0x0f67eff6e6b3718a60f4c58f3ee545014e05194beff2f3e75751393773068ffa", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 116, + "address": "0x79bf225fbfd40f78b1878a6d1eec1bb03df92aeb", + "amount": "1290807000000000000", + "nodeHash": "0x554d06155b8ccf45cd1f2f293f8eabdacc0e0b423021837c9c5035b5d9d758a6", + "proof": [ + "0x5552493a56ad6f1ac4a7d42f36cc6c8b1ecfa6bbfc09e1f14824e581b86cd5db", + "0x2f92db0d3e17968df7094e69f99c51194c9236a1bb9f30ae59bb1124142b4b21", + "0xa29dab6c266c58115bc9610428c6698333af363ab59f695850636c30b506a079", + "0x58353663046a578dd1f9997dc38a4b7831c0a0b3796a5c26135ad8e0b4324c80", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 117, + "address": "0x66ec8503b4f9e3fd998bcc20ce9d2b16bb8c370b", + "amount": "1264464000000000000", + "nodeHash": "0x6928c1c641189630d392ea596a92550e2bf1488854a0cec7890a0684cfbb7c34", + "proof": [ + "0x67c5fa2871cdee9ed4bd3270cabc748d221a0d100cc4ac7567fa3999724e934b", + "0x6afcfcf0b1dcdc884ededa3dfdfe729c0fcc28f2e1d1694f8118b2a10f1fefc6", + "0xe2c4b5e85f6c1a15c4a2e920b160b9db8f2be4d562b8fce0fcb9b0249335bb14", + "0xbef3adfc06e9c1b99f0036e6bd56baa2f939d20e0f53ee8ccdaf789586255173", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 118, + "address": "0xab99448b70fd0408ba0114060e6410b56cfb145a", + "amount": "1264464000000000000", + "nodeHash": "0x61236644a38b84cf5e9af0de1272bc43e1d41e1c1b8f686c4d6e8c083dd0181c", + "proof": [ + "0x60310e3189b01a8eb478998f052ed397c87e84a8e898ef76430dd8e238a128a8", + "0x3dce93286ba0d83852280a31a35584e2d00e320703d859d82d14d4d769e2f44b", + "0x7ec8f9e5ba1f89b9c48f931eb68bbea8c0cf12082f3f60cffe31bc43916aed11", + "0xf97c70e9a61a1af1e7abe2d3dc3538c1ed7c5556d96b05a481e88adc38bbb1c3", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 119, + "address": "0xb7b1cc940cc88640089028d4910de22e39e6d117", + "amount": "1220559000000000000", + "nodeHash": "0xdaa87e4172ce68ac16192402dbe8b0a1159aab6eb60c086de51116455210ff2b", + "proof": [ + "0xdb2b7b073262e535dbe378364aec03a231dd96e20f4eee5891fe018f1e445bc9", + "0x68db79bcfafc2c976ed6a8f104c18538c68c7c86abc7dc622180bef12da072d2", + "0x12d18da5dbeb6a18115947a6a933788d9f4fe3a2d151d55e55a533ecb360888c", + "0x2b7b07c087af29ef8f3779ca825e620e6a91fc1b8119a477cb9747b5bfcbe5c0", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 120, + "address": "0xa5eb5d543fca821abfc3ae65bfc20ee9b83a0c02", + "amount": "1123968000000000000", + "nodeHash": "0x97c6c981281aa38a2f04602572b08ea7b5ba778e148b17beee8da2a18ad11938", + "proof": [ + "0x98b68aa8d362dc3d5d70ab7758ecdb7438d306d1a5f5852602feca94c85dd5b7", + "0x2d2d1ce318032af693f0fffb3731d73776c58111559cdca464596dc0e30e2535", + "0x80d3320efcf13447783dc7c21219a96e1b9acfb649c33c72903af272ff3b6f25", + "0x0f67eff6e6b3718a60f4c58f3ee545014e05194beff2f3e75751393773068ffa", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 121, + "address": "0x7a193570f7162313f3c7304bee7584e964f1b6f8", + "amount": "1062501000000000000", + "nodeHash": "0x66991fe1217c6262720f2f0967aede2fc8d271196e4b9e34afe0cb4a07dc4258", + "proof": [ + "0x668ef62aa0a418edda587f7330ccf98806cfed1042b0d913141e7c1bfcfbf16d", + "0x2ba411f4747985679639d2d4cc9e92a9b4e818377e32e3cbb935fa5eaeaabbb4", + "0xe2c4b5e85f6c1a15c4a2e920b160b9db8f2be4d562b8fce0fcb9b0249335bb14", + "0xbef3adfc06e9c1b99f0036e6bd56baa2f939d20e0f53ee8ccdaf789586255173", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 122, + "address": "0xf7adc901c921ca4eecc0f95a26aec81dacf57620", + "amount": "1053720000000000000", + "nodeHash": "0x07087e3ca81520beee67237b32b53372f88d40db003d9ef3ae043bb80dcfb99e", + "proof": [ + "0x09ba9a82855bff1513b46a6063e38af605f1c36d53df3674058de45b81d0543d", + "0xb788935e7bb4bacecd7d56f61f8db99689026fb698ec7870dfe58fd4fd61cdc3", + "0x2ce668e3cdce60660255a67e7d4a5edf5412f5f39d06875e56a07a4c1d45097d", + "0x99927aaba68fa1c526c8be885fed2a583000a016f39e6210564017b20cc96842", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 123, + "address": "0x2b3990b105314339b2dfa8296715024f7693bf49", + "amount": "1044939000000000000", + "nodeHash": "0xf7a3e9eaee0c02f3b74f7d9e096e9e790b89a17c0ac2b1426aee72df30d1c740", + "proof": [ + "0xf7b62fd09c424bd542e2166f7a4d3e8f7b31ac97ccb8a1d6cd3a1b2826f1f6e2", + "0xd7367cecf0b43d01d848ccadc0bf5a3c2be0404e254afa404a6db5cd55530bc6", + "0xee9e134d432662feaf476361874386ca78329b4992de686bf58e678134ebeb7e", + "0x4374227d00e14e26a6659c58e26ba194704a9c4428299eefc28bfb5fbdf9f12a", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 124, + "address": "0xf4e262a2165527727cb51e6ecc0b103f14ac4cfc", + "amount": "1027377000000000000", + "nodeHash": "0x815feb86ee20a6d008778e6ffa6382d189cd7c1b3f60bf5bc30deafae4f44f0d", + "proof": [ + "0x8063fcae6ea90f3b8621b4d955a07e5d9f1b0c1723b8f35450807ca81d653947", + "0x345c7478f3f9dbe03e37b5482bda9e03fa6246419c4ddc4cd780b55992dbfd3d", + "0x5328a6b8be51d45cf7e2d0f283625bf5829d066050211d2e89db5dc907c1305d", + "0xe92595020625bd816794a0573140fc5d9e5dda15bf5439f01004d872a3811f20", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 125, + "address": "0xaafdd768fa83ef7af0e19707f2be9d1db1924766", + "amount": "1018596000000000000", + "nodeHash": "0xdb2b7b073262e535dbe378364aec03a231dd96e20f4eee5891fe018f1e445bc9", + "proof": [ + "0xdaa87e4172ce68ac16192402dbe8b0a1159aab6eb60c086de51116455210ff2b", + "0x68db79bcfafc2c976ed6a8f104c18538c68c7c86abc7dc622180bef12da072d2", + "0x12d18da5dbeb6a18115947a6a933788d9f4fe3a2d151d55e55a533ecb360888c", + "0x2b7b07c087af29ef8f3779ca825e620e6a91fc1b8119a477cb9747b5bfcbe5c0", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 126, + "address": "0xab30f11201d6d53215729d45dc05a0966c237922", + "amount": "1018596000000000000", + "nodeHash": "0x79093ef1d89e28c56c31ede158d644c3d3fbb5ad6ca8f2f46287197560f194dd", + "proof": [ + "0x7907d24968c9858254fccbee98997f19c79164cc9df3e5d65571bb47969ed2ee", + "0x3230d530998fa5a1d112dbf7b97358852723757a900467d431842f692c0646a0", + "0xa2954bce93ae1f34dedb0c78d5f07a45d696775993a5716f85f184e6114bf588", + "0xe92595020625bd816794a0573140fc5d9e5dda15bf5439f01004d872a3811f20", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 127, + "address": "0xa09427e2db1844b0d5d9b8be9cfaa308d6ca893d", + "amount": "939567000000000000", + "nodeHash": "0xbdda69c9ab1972d1dce8eb141ec1df9ff6e6b5cea3f14e2a1b1aec2a46652b2c", + "proof": [ + "0xbc1c0bc7a2e86145e4ce2a39d03789d25adfdf26df51a47f2a0c8024b6719e98", + "0x7508527f3d539c334a7869afcda6c5323290982bc7a4ad233a24c0ae7c33970d", + "0x4e2cf104325c508896b8c5cb332f174a17e5f8b29e2c5c58a1244e25d9ae856a", + "0x31d945c0d30504036c02fe1f0dc3952db23c29e3d87e59d1a06565ce6dae2924", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 128, + "address": "0xbaf71b1db54c2fd0c4282af17037bf83a1a3032d", + "amount": "842976000000000000", + "nodeHash": "0x36fc90f30c9be3cade5b44aa58622fa1084a838045f62a71c55e5cfa68ae3dc5", + "proof": [ + "0x36e67cb55c255c0ebad42621d5fe2e9e0d8bed9f687b5ad4e6af2259248a356f", + "0x33ca0790f218c81d7e3d7c63e0a28f672d57816fc2ec41e5bc1c86ac94cbacc3", + "0xffd4f9d79518701e9fbb847603851d8bcb3af375c38eb6e9de63aa69d5b362e3", + "0xb149db133f2de53fa1c5bfb7817947aa4b91fba91a7f427e3cebc44a51c0bb8a", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 129, + "address": "0x7b50e42448a2541e71f1f7bf8516595ff50a74b8", + "amount": "728823000000000000", + "nodeHash": "0xbc1c0bc7a2e86145e4ce2a39d03789d25adfdf26df51a47f2a0c8024b6719e98", + "proof": [ + "0xbdda69c9ab1972d1dce8eb141ec1df9ff6e6b5cea3f14e2a1b1aec2a46652b2c", + "0x7508527f3d539c334a7869afcda6c5323290982bc7a4ad233a24c0ae7c33970d", + "0x4e2cf104325c508896b8c5cb332f174a17e5f8b29e2c5c58a1244e25d9ae856a", + "0x31d945c0d30504036c02fe1f0dc3952db23c29e3d87e59d1a06565ce6dae2924", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 130, + "address": "0x7223e76b2871a3c41202472fb2cec92ad76ee767", + "amount": "702480000000000000", + "nodeHash": "0x1c7695ef4b33446b2866c11c50374e3ed79ba3488bda0ae639592ec43119bd67", + "proof": [ + "0x1e997a55b05bdab2a284a5e65c9911be652bf6df160059172c683585f8790fed", + "0x25ee7413e5ff15c1f681c4be625cdc805d06ce01ceea396542a6bbcee9deac17", + "0x52f131eb5f23b186983cc1103bdf44f5286b58cc5da6da8de3fa399de2a0459f", + "0x401b4840cfd7296b17a0bb231a8ee94c8c8fcd710eddb6978d46e0faf47d8eef", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 131, + "address": "0x83790d6be1744b2b70af28ce8b6f96531f55808a", + "amount": "658575000000000000", + "nodeHash": "0xcafc47fdb73b0d633af14b9a99ebf61636ee1fa03628b0aa1a7bea057b6b2833", + "proof": [ + "0xcabe4d6a7bff86d35ea8826bf66fbf1a20ef5eaff09ae3f62388611280289221", + "0x074c3a51655a467b790f342405569752ed9305060afb988c4cbb7b1a0b61c2ad", + "0xba770e25357a76c9190bdccfaf3cfbc9f10ba8bcf8777d82c64997ab022c0e3d", + "0x049057c435e918361a347a9fe31a6f48819b657ce283c5390310bbc944753eca", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 132, + "address": "0xee4c633368f1fdbc0be5845c32dd592c40368bb0", + "amount": "2063535000000000000", + "nodeHash": "0x12f74f4f36ca93cd83f83fcd5d38c6a5a7b0c75d8c4880bcc9be7be2b42c8601", + "proof": [ + "0x114c90054a873891468d67e2c1ad6a54ff280056e11588a87ef498a0c22a4c75", + "0x63c2f004a196c062b79f867af6c4c91a851891347e2e60d885598c9f7fa54bad", + "0x1852df03d73c3d1dd34606bdf92d30c73d72be657b7e791005521cc89c201bd4", + "0x3568ae0dd2327f26f3bd4c1af82d9a7d8132e50855cdc1c20f276776264d6ab5", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 133, + "address": "0x6864d15cf60095395b771b2f88a02808e28b4ff3", + "amount": "491736000000000000", + "nodeHash": "0x668ef62aa0a418edda587f7330ccf98806cfed1042b0d913141e7c1bfcfbf16d", + "proof": [ + "0x66991fe1217c6262720f2f0967aede2fc8d271196e4b9e34afe0cb4a07dc4258", + "0x2ba411f4747985679639d2d4cc9e92a9b4e818377e32e3cbb935fa5eaeaabbb4", + "0xe2c4b5e85f6c1a15c4a2e920b160b9db8f2be4d562b8fce0fcb9b0249335bb14", + "0xbef3adfc06e9c1b99f0036e6bd56baa2f939d20e0f53ee8ccdaf789586255173", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 134, + "address": "0x0481bda39e00bcc24ac276903bcdf3893d8a97ca", + "amount": "421488000000000000", + "nodeHash": "0xcabe4d6a7bff86d35ea8826bf66fbf1a20ef5eaff09ae3f62388611280289221", + "proof": [ + "0xcafc47fdb73b0d633af14b9a99ebf61636ee1fa03628b0aa1a7bea057b6b2833", + "0x074c3a51655a467b790f342405569752ed9305060afb988c4cbb7b1a0b61c2ad", + "0xba770e25357a76c9190bdccfaf3cfbc9f10ba8bcf8777d82c64997ab022c0e3d", + "0x049057c435e918361a347a9fe31a6f48819b657ce283c5390310bbc944753eca", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 135, + "address": "0x3070f20f86fda706ac380f5060d256028a46ec29", + "amount": "421488000000000000", + "nodeHash": "0x789db8691e99b510e2ac94745a6ffe9eda19e78ba0eecfe1795f6c66de8328f4", + "proof": [ + "0x75921cf31c0761691e6f89d99f3f1bb25513f3bb3450e1d6ca5ca7cc7e0877a9", + "0x46eabe7b9e80724de8e4828d9bbbdf1686f155e8d2e47ba37f46a799acb9eb23", + "0xa2954bce93ae1f34dedb0c78d5f07a45d696775993a5716f85f184e6114bf588", + "0xe92595020625bd816794a0573140fc5d9e5dda15bf5439f01004d872a3811f20", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 136, + "address": "0x4f92457d8a7a363c625e76ded4121b2e6b9dd74c", + "amount": "421488000000000000", + "nodeHash": "0xee8c39b82dd589f111f4b015501c6e890d7432754eb3140c815fdf4b02130c55", + "proof": [ + "0xee05e205e873d61918fb919f9722d079b03255874977054a3ccd55d3c73bfec7", + "0xda393665de1c0e94aa5468a8de6c20900366fde499a56316c6fe3760bd40c712", + "0xbcb103468201a2c28e4f0287614283c7f71e3d3caccb0d84054ad77dd6f8268b", + "0x4374227d00e14e26a6659c58e26ba194704a9c4428299eefc28bfb5fbdf9f12a", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 137, + "address": "0x2e48b4c77a45b3c4cef588465b6d2fbf1b2b85b7", + "amount": "421488000000000000", + "nodeHash": "0xfcdb017efdd5c8d8ffa90985784ac74180360fac86bafc3b6b10012bf332ea50", + "proof": [ + "0xfbd8ee2f00013df211c070713ef96b94fe62f9884b624fa3ca3bf716e1c6a2df", + "0x1d413e56e8e75bc21af5f8c3f4af12a9dbaba6944992f27cd32a51871ec0c68f", + "0x8a190fca6b4bf18ee70ee088347d1d5ce46e23942d97e6d45cff623f33b56a8a", + "0x878b93f5379152f0d671b155b7a722b05eb555558bbfe56bfbb394a485c7952e", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 138, + "address": "0x39f6b2d21a5f93291fba2eb2cc1e435d21bcbc89", + "amount": "298554000000000000", + "nodeHash": "0x4e717ffed58d85120beea2e8265d9d846b255d8c14dbeb7c14eba03903300b0a", + "proof": [ + "0x4ce75cc58a99bc1319e030c00e419878e47a86d7ece12bb50d2d95d69c4f8741", + "0x433d42ba2b9b347e7209cfe76c6616bcc97a25e92c1790cb09f81ee6a08930ef", + "0x628f4d9731d41648d4ca18d657beca29b7b92de3ff4b8838cbee00e38ccc4ad0", + "0xf5206d1a8eea30d8766d8236039d92db78f10d58c4069671da4386cc649fb9cb", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 139, + "address": "0x3cacb4468b53fba62bb30ed5975a78864ecf5ff9", + "amount": "61467000000000000", + "nodeHash": "0xe9ac2526e391f9357b4cacff1be1b29233a2ff71f290d4133ad33619f1137ccb", + "proof": [ + "0xea19c735384b74ceaeff0ed09ed5864e3cabab8eb77e59111047a450b2d8be0d", + "0xa72a53caefe3e0fa4bffc908960ab58091b31b55fa12f7505dc4941dd7207cab", + "0x31671f7e335400af5b05a1bb5bdfb79516d12b6b57500a1d7aa1e53bef10935e", + "0x3f59b8cdb34f4bc3b9c931471fa7d84b69826722d8401478e0c9afbc1f56fa16", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 140, + "address": "0xe1e5072de1d9b120cc33c57ebadbcd33dbc6dd62", + "amount": "43905000000000000", + "nodeHash": "0x7f51f142130c512371f880cc23a7741d6e72ad2b893d2c983bcd72e03d3765fe", + "proof": [ + "0x7f33be7b04ce1286a831cfa6c95f198695572895bbf214ffb253010a2d64c1b9", + "0x1e44d923fe4c1a693b50a7476b470ab1f7761bfb2c5ab5f6b874045f440bcfc0", + "0x5328a6b8be51d45cf7e2d0f283625bf5829d066050211d2e89db5dc907c1305d", + "0xe92595020625bd816794a0573140fc5d9e5dda15bf5439f01004d872a3811f20", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 141, + "address": "0xc5f16770dcc9ce6e6f2e02f7afdc101b28926308", + "amount": "391351608000000000000", + "nodeHash": "0x0ef4f9145ea91c5bc1afdea4c8c4cae3f448a697ee2c7efe080c533326eb9e70", + "proof": [ + "0x0f574ee681925a9c69497c6042e01fe9f219a65f4a23ba48bf5b0d331cbebcde", + "0x61f53fec17bce18fd0593855b0bff78ee1a8dd86238ef652eec0f1ae248559ee", + "0x27f5ba055019acd91796b1eb1f12fbaa9b667c42bb565c2b0aab0809e3752977", + "0x3568ae0dd2327f26f3bd4c1af82d9a7d8132e50855cdc1c20f276776264d6ab5", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 142, + "address": "0x6453a526dafa5788ad09cac4a5ec9044d3684de0", + "amount": "90953598000000000000", + "nodeHash": "0xff8a6d8cea89c87f8a2ffefcba2a3b026248ac5c1fef2cb4ef8677731a884fa2", + "proof": [ + "0xfdb9ae7334e8b0102e2b0cdb16a60fc5b2b651cd86711baf3ab927b0395f3dff", + "0x049b5b005b9b66803cbf02600ea7719af80de201958c535f372f1cc3b2e60bd2", + "0x15e1e45a6fca8e1c8e57ddc39a2962d2260850882b5c8b04832d1dd821cf9e27", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 143, + "address": "0xba3e737ef7ceafe7b586cc9da55fe36029006edf", + "amount": "84569811000000000000", + "nodeHash": "0x750ce3cfcc3c375c355366b818b450d9e937aa30a2d9789cc89ce05cfeaba647", + "proof": [ + "0x746e3770d2746748a307245379404830d7b388f6f050b9f2f851aa74d380ac8e", + "0x97165447885600a06e18a2da7ed0112cb0164976a9bd0208beaa8773b89f5079", + "0x27e457d39207c5c9413452f66d1439674315c7ac031965edc3ef5983d37b3479", + "0xcd656b995bfbb006bd6cc7e0553e7b43a8a428952ce7e40550495ee52cd3d9f7", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 144, + "address": "0x5fc79e21ceca2aa0f7a0aac71ef3ddde8f004e9e", + "amount": "80978382000000000000", + "nodeHash": "0x5360a65b13a4457735f913e59059a7bbc5aa42175bf4084e8bd488b2094c7f2e", + "proof": [ + "0x547b3791b7b54d8fd2dd80a3ee836ecc231ee707bfc7cb6083933fe92d7b23e0", + "0x4820465c864e10629d6541621ab13ba0910dbd01c40270687fc205791d3b9a38", + "0x4d72bdbb948be8a9d3a660d43eaefb5cccc88ede0e152cd8fed46d934ad5d0b2", + "0x1d51ca0e19aa543a6545b29cab8ac5f9483bb15f0c024a81b9aadfeb29b9f54f", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 145, + "address": "0x664dd5bcf28bbb3518ff532a384849830f2154ea", + "amount": "73848210000000000000", + "nodeHash": "0x6a0227afb109fd35bc100d93a978262714b40d7e1285c99755b761ed2a807a89", + "proof": [ + "0x6af043512246e059286f5c7298e40fe58946671677a6fc4cf96a9815fdcb6583", + "0x0d14719848a014eaa8f0c734eafd7d13dcefb69907a041a7469e38037977c861", + "0x428c48ad33d483236e7b022672c662d2cb14b43b2cc565e3cf513754d2973895", + "0xcd656b995bfbb006bd6cc7e0553e7b43a8a428952ce7e40550495ee52cd3d9f7", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 146, + "address": "0x2b2f2a9302cdecdb7d4a2e7005336f7b345c1092", + "amount": "72135915000000000000", + "nodeHash": "0x50f087d27be667e26602cbfba7e50ad22f33ac898610f10192a9da5118f0e95c", + "proof": [ + "0x53246a05309962591b4b538c57aa4e4b1b6889d37ed4d9e9a67c30ea9673e46e", + "0x21e618612ffca0a2548757633189cbcde9cc6b1278db931633e13d1d8889b27c", + "0x757b5f89f8f0c01453b56baff0bf23c0613755707e6c33d7c6a462624ede42c3", + "0x1d51ca0e19aa543a6545b29cab8ac5f9483bb15f0c024a81b9aadfeb29b9f54f", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 147, + "address": "0x0059efca9d24bf579ee0314e7bc2569674aa42e2", + "amount": "79292430000000000000", + "nodeHash": "0xb19015b5bb6d039d5f21876470631ada1bce518e8c9c758821ddfd7e91a20cb3", + "proof": [ + "0xaf0c3f1eb99a3f67796d0efcdab3180514928125897eee54f14740df85f1bae9", + "0xf6289c4c79b07ffceb612deb5609ccd9ad5cf10fbfdb6ee13dfe1156497b17c0", + "0x7a5ff3f19b447c6cd8d97470d640993dac0e19acd049d11248b6c33752696a5e", + "0xc9285e31e87d3c235fc0dc4ab5c5978891a414a32a26c69b43d5834adf7fea91", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 148, + "address": "0x6dccde9a3611e42f7a0d3bc82e5385a29eac8411", + "amount": "35343525000000000000", + "nodeHash": "0x296a58cd4e848c7c8e90b77a802ba45d9badac8be56b022b01e72f22493bbcf1", + "proof": [ + "0x26a57a02464b66f3d50c83dfab7ed2a636e06da38f0917079e0775670cfeb0b6", + "0xa23c11acf3af8d6541fe01115721a53cc02aa64c8711d488ea83aa785019c612", + "0x8b17088d53d8cd82f15cd19fd878ceb428963b30615a65fbe1afc21c9fac39d5", + "0x1d9b0c09a3cb63729974fa7589d3e021eba7ef36ed2ec96be004ae76e40e4456", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 149, + "address": "0x7e2a3694256e13705eb7924649e5e267f5bc5398", + "amount": "41103861000000000000", + "nodeHash": "0x2127aa5e2e753afc65b44f48745490a96653895f0ed062e61d0a1c3d16996b72", + "proof": [ + "0x245eeef83b1665e81590fe99607b4044dd22fa66c5810cf62db42daa276018f0", + "0x0ea261dfafa4d2569553c51c317b3d522843271443a3d425f82cba4c331dd15b", + "0xbce9650f2ce0bdddf4d157210724cbdd8249457433191a81be341fcaf6c64a30", + "0x1d9b0c09a3cb63729974fa7589d3e021eba7ef36ed2ec96be004ae76e40e4456", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 150, + "address": "0x888851dc68c2f675c48b9967dd234dbe0e955fb7", + "amount": "28819242000000000000", + "nodeHash": "0xa9ba4a46fd408763049e011ffaa76e554eb516078fb35be6b781b1034dce27c8", + "proof": [ + "0xa81db51064f0b40811706f68450f1cd023feba364708c485cecc0c07875a2793", + "0xa3acbd14a7e5ea26e2c9647588d9688a285ddac6f54c1aa5444d27e99ab71c7a", + "0xce2d66202d26fe1faf88499baedd97a1f2f5dd90211b19a3d4f77e9b88291311", + "0x77aac907780e1eb7ed08bc6fb93c7a472125d7cd0e8720e5b60c436f725d4b01", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 151, + "address": "0xfb7c1d49e006eaddff2385c7ef8b0c5cf49d038a", + "amount": "27027918000000000000", + "nodeHash": "0x2ec1369488455fb59365c2659324b21fc54f4aa1fd45dfb61fb4c324ad8ede5f", + "proof": [ + "0x2e43d884f380a5b0e9ebebdeaecd5db76d67c4b710b58ec673c3cd33676589bb", + "0x3a7ea8bbab2341f461c79eee187d8029cc9ca76b1fa8383cb06e5abcab881cf2", + "0xae70dc4f1661eb4cd5ce6ce92d37555b22131bab4d1e2299a12a96bb693592b8", + "0x3f9c8e77affbca217664af93969c37b75db5f5e970d56dd0a2f7b374a9d8c12b", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 152, + "address": "0xbbc818f10b2bd5d4ae1a8e3717f6324acee6ec1f", + "amount": "23717481000000000000", + "nodeHash": "0xb1be9ee1d722771394b363bcaff4210ad413be016004f1b07ab26e905fd0c894", + "proof": [ + "0xb3c688ffda0e6a31abc67ded87a2dc5ab226c3f5f98854884b299e64cf1e1609", + "0x9d3ccd9ebe1be04d0b3a6d06b85581c4958cdc3f1d5e2fdc1f24da87afc7abda", + "0x28f06e02ae1339fc54b9c964d1b5ad118e18997a61fcf36511d31cdf927cc508", + "0xc9285e31e87d3c235fc0dc4ab5c5978891a414a32a26c69b43d5834adf7fea91", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 153, + "address": "0x4527c75dfc50a5390dc663f81b627c39e7b9acad", + "amount": "31154988000000000000", + "nodeHash": "0x39e187da7c06c5bdedddca51726df59bd01a8c406c915cb5430ca9e776c869ab", + "proof": [ + "0x3ac6158df44f651b423f622413095831b356ae735eb8d7748d5cfe2b06641576", + "0x48bd5c8baa8450c942ed65eb641cfed2882d14944565066fc28fd66d41e5cd2f", + "0x9ee3fa7c938654616c76fc487fceeed95be956662b33deb34a3b9c277f0408f2", + "0xbd97d3672cdd0dce515b28377cdb7ddfc5d08d2a126f287bf0f31ad87296aab6", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 154, + "address": "0xebc462355194903ad1ad32fa767b3b74d5aca278", + "amount": "21495888000000000000", + "nodeHash": "0xe9428be0a2ba85e20788c5cf37a24bc91f320e6d2e56a38cd9eac66432739ed9", + "proof": [ + "0xe8c0611d5a166d83fb7daec5338d4bda952e97e744f02dfaf7d34daebf4c3917", + "0xf857ecb5b3daccfa04d13f43c273f976827b19767b0957dc7958d9f94ef23202", + "0xbf8af7b1dbe7d5a234d0212ab5762422bb4c90ff91e57449a0ed92527cd64ad0", + "0x3f59b8cdb34f4bc3b9c931471fa7d84b69826722d8401478e0c9afbc1f56fa16", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 155, + "address": "0x369ff079a6f92e1e0cbbe2e46b41fc8182584fe5", + "amount": "15445779000000000000", + "nodeHash": "0x9072b0182a81030d746de8131e09b56bc2bd2e527ac3d5b135f9b86b97b59867", + "proof": [ + "0x908b0ceefeecb2610010684023b7556344a4472c58b420e3fc5143a80e8b67e5", + "0x2f542ca88e4d38bd8cbcf03977aeba15117005dc007c45b882cc0661533827cb", + "0xaa377edb22f3b6832c85b39443e2addce35609872bf62447d5b482d94dc69eff", + "0xcdc7c986bef6799a7b7aed7a38307c79b834bfb880319aecc678d4f1a76f3473", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 156, + "address": "0xf46b758c25484a6170227a0471a49077d3a1dab6", + "amount": "14646708000000000000", + "nodeHash": "0xae8fd7743d5f2245b8bd0572e464ab847cd16490da7d05c7eeeb7e183c259a0b", + "proof": [ + "0xaeebd41522c521b42f2379e13a34069f5715c75f6c90297da466743d08540f30", + "0x8ce3efbd5f46277dce9ed07d7992e9cd790ef01d9fa2817bec4de9f0cce83cd1", + "0x7a5ff3f19b447c6cd8d97470d640993dac0e19acd049d11248b6c33752696a5e", + "0xc9285e31e87d3c235fc0dc4ab5c5978891a414a32a26c69b43d5834adf7fea91", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 157, + "address": "0xf2dec80829f006b3d8e31967f00db4c818e54dc6", + "amount": "12381210000000000000", + "nodeHash": "0x65c1cefeb0f49053ade79a75fb7267fa57a9b3e40dc0fe58ae67fcfed31ead0a", + "proof": [ + "0x66096b750d267fe5a42a20291ca034c628fc5e435c184ac09c61f2f9fabcac6e", + "0x43ea64ea828c2a81f4a0dee2f0a601b72bcb2d4d276b13489a1a35da50168959", + "0xd2aade99c8f850b3a3bbe9cf35336218f89132a869dd4c4641d5e9f5bfee5f9a", + "0xbef3adfc06e9c1b99f0036e6bd56baa2f939d20e0f53ee8ccdaf789586255173", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 158, + "address": "0x992f8e145c5404e944aeb8f53660985daf9dc827", + "amount": "11968503000000000000", + "nodeHash": "0x908b0ceefeecb2610010684023b7556344a4472c58b420e3fc5143a80e8b67e5", + "proof": [ + "0x9072b0182a81030d746de8131e09b56bc2bd2e527ac3d5b135f9b86b97b59867", + "0x2f542ca88e4d38bd8cbcf03977aeba15117005dc007c45b882cc0661533827cb", + "0xaa377edb22f3b6832c85b39443e2addce35609872bf62447d5b482d94dc69eff", + "0xcdc7c986bef6799a7b7aed7a38307c79b834bfb880319aecc678d4f1a76f3473", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 159, + "address": "0xb7d49adb031d6dbdf3e8e28f21c6dd3b6f231cd5", + "amount": "9773253000000000000", + "nodeHash": "0xd98132967f4213ae7d40892abed5886f93e86167425f244c292d0070e322770a", + "proof": [ + "0xda86eb0650d7c85c40bb685e89f8560daa3b7ce910638ceb9e341103ef5f6030", + "0x0a6fdbef907c63b8b31337c51d31be9b4c94b8c624534eab843080b4ba13d492", + "0xab2c221ee4dd5e1ebced8623292417356a6da943c17720d5865ae17a0765e4d2", + "0x8c48e5104ea64540b9742e7e67c632558ee596ef39aef3dabc5a750b5a0d8de1", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 160, + "address": "0xe3e39161d35e9a81edec667a5387bfae85752854", + "amount": "7516536000000000000", + "nodeHash": "0xc75ec40dbe45bdd4424e9a2c8746793957b9dd9bdf5758bab70d0616a178501e", + "proof": [ + "0xc7f76a84e0a74051d67ae9f1b725f9b4b603690e50bf1bcf07dcc46ba6ab3a74", + "0x7ea73309f2675c73d72dff2898e3ba796b71f0892aced82e0b582a4c1d2223eb", + "0x1de190c06efaa371a311ee112f45007771a2745d514c4f0209909371267989c1", + "0x8f1e0bca0b330c8a6044d203531a4fe59749a3d5ebccd1a4a524fff2f807ad3a", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 161, + "address": "0x5dcd83cf2dd90a4c7e1c189e74ec7dc072ad78e1", + "amount": "18835245000000000000", + "nodeHash": "0x5f42b29d1228f686214b091d51ec7c472dae6e1f0bc4f5263db1a7b1262a3c68", + "proof": [ + "0x5fbc134ba0a8f0a9e29246f8673fe47d345a564648b43da05a3e61281604b182", + "0x9e4385a14841fcac5ed1fd7854f7c22ab1439c72f3b94a5bca07367fc805d646", + "0x49caa557cb6111a0eda5b1f13ecfa94fb09fa036e054a1faeac9f3ba92b8af6b", + "0xf97c70e9a61a1af1e7abe2d3dc3538c1ed7c5556d96b05a481e88adc38bbb1c3", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 162, + "address": "0x53e13df38ac96c6929b98d069502d74fd2228c27", + "amount": "9290298000000000000", + "nodeHash": "0xa7d254ee883dc59d29c71bda776408b45f1a9fd4667e5b182cba20b15133dd63", + "proof": [ + "0xa67780f37b9cd523d9d53527cf878d6667701c8a221a8e963413c75a1c54d64a", + "0x0a779255d0c195dadda520ff17d0441b331b3153c60f936355415d6a7402e2a2", + "0xdd3426b4e15a8f4fd901853093b9b07e6e42020adaa75277dc0147e59a0f8290", + "0x112c5e47475d52c67bc98d675e1056afe3e6623117b94507e29be69ebf20a435", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 163, + "address": "0x71b9ab61cad1b6d4f6dafcd795f346db36f3b55e", + "amount": "6401349000000000000", + "nodeHash": "0xc499f325162a45963553fe1831882aaad5fcf7d4783d291f61bed6245535bf83", + "proof": [ + "0xc317c3194d51f1f55a13515836500a5cbf2838a8e1d65616c50ac259c105b056", + "0x5c8acf1eafde10dffa8fecee651fda48d6a7e733b5e06d15c14775c96f07d392", + "0x888709e4e04365471499a37df65d5d44269f0a6015a49783f99d0e1487d8a89c", + "0x8f1e0bca0b330c8a6044d203531a4fe59749a3d5ebccd1a4a524fff2f807ad3a", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 164, + "address": "0x8a9bcdc68d19e111663d6c986caf1da1eda1c304", + "amount": "6594531000000000000", + "nodeHash": "0x64e5c55ef03b40dd1da750150cd4d39fcce3c48857667b5cf12ab2dc4ca0e7b8", + "proof": [ + "0x6591937ab2f9d12654f5f20f6a6f6d1d91ce19dd894d496a8d454a59fba4ffdd", + "0x7cfa616b6213a643562e2be4b086eead77c318e23db11782c0bfeaab8e366dc8", + "0xd2aade99c8f850b3a3bbe9cf35336218f89132a869dd4c4641d5e9f5bfee5f9a", + "0xbef3adfc06e9c1b99f0036e6bd56baa2f939d20e0f53ee8ccdaf789586255173", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 165, + "address": "0x10340db8bd18401b65d1178ba45d7ce3c66aa6b7", + "amount": "5409096000000000000", + "nodeHash": "0x759129628da533708c9f7b5e8e582a9864f153a980100a24e0e2d38438f65ad9", + "proof": [ + "0x758f6cc6d03a96bca46d15666d7b0f61fc564708c1d74829f80daf001653538d", + "0x8499959fd748e0ddbc896423ff3fb98b83e44ed413aea1135eb52584d5155ebc", + "0x27e457d39207c5c9413452f66d1439674315c7ac031965edc3ef5983d37b3479", + "0xcd656b995bfbb006bd6cc7e0553e7b43a8a428952ce7e40550495ee52cd3d9f7", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 166, + "address": "0x5eff77571184427f5a9e951597571b5259820b48", + "amount": "4829550000000000000", + "nodeHash": "0x4fe684cb933bca6ebce70a8a5d2805128eaa0681a931cbb31357a08b139c5d86", + "proof": [ + "0x4f1f84e694e849d9208a4217979ee0e2d7f41d55b704960b3801d138ba83a6dd", + "0x2a658f536b92a8ae35d0beb6befbce9b91af86b348291522a1bff519ae67799d", + "0x757b5f89f8f0c01453b56baff0bf23c0613755707e6c33d7c6a462624ede42c3", + "0x1d51ca0e19aa543a6545b29cab8ac5f9483bb15f0c024a81b9aadfeb29b9f54f", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 167, + "address": "0xe65f3930aa2cc8711ef0d8c2244978a7f4a66c6a", + "amount": "4118289000000000000", + "nodeHash": "0x7f33be7b04ce1286a831cfa6c95f198695572895bbf214ffb253010a2d64c1b9", + "proof": [ + "0x7f51f142130c512371f880cc23a7741d6e72ad2b893d2c983bcd72e03d3765fe", + "0x1e44d923fe4c1a693b50a7476b470ab1f7761bfb2c5ab5f6b874045f440bcfc0", + "0x5328a6b8be51d45cf7e2d0f283625bf5829d066050211d2e89db5dc907c1305d", + "0xe92595020625bd816794a0573140fc5d9e5dda15bf5439f01004d872a3811f20", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 168, + "address": "0x2038c11f3132ac62ae062984ac0d8a14fe2e096f", + "amount": "3969012000000000000", + "nodeHash": "0xc5ee73508f61a2179ee2606a639adc0b7f591eb9fbf96bb9f765243f7fdf24cb", + "proof": [ + "0xc6f430587ff363e0814b5b3ac1d8eaac9b7715b6fd2868876bcdbb63596bbe89", + "0x03ed36727f0091bce111caa9a4ee983b738b6a2826332d3b9e42364adb782605", + "0x888709e4e04365471499a37df65d5d44269f0a6015a49783f99d0e1487d8a89c", + "0x8f1e0bca0b330c8a6044d203531a4fe59749a3d5ebccd1a4a524fff2f807ad3a", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 169, + "address": "0xe8b2d43eb45074b1dfac494641e15791b32d27a6", + "amount": "9887406000000000000", + "nodeHash": "0xe50c0a6866dfc4fbac9ebe9509ca06c14f511864e34a130fbe9788a66925b34a", + "proof": [ + "0xe626355269371632b18e02d63b674ecf90ca397d832a86e13f04e387136d6837", + "0xb7543e976bc75d1fdb35f7e3375de2d8f4d636569988089f030c07088a21544b", + "0xbf8af7b1dbe7d5a234d0212ab5762422bb4c90ff91e57449a0ed92527cd64ad0", + "0x3f59b8cdb34f4bc3b9c931471fa7d84b69826722d8401478e0c9afbc1f56fa16", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 170, + "address": "0x5d97f3a8bb293f9561892091779ed6af0df209c1", + "amount": "3222627000000000000", + "nodeHash": "0xa67780f37b9cd523d9d53527cf878d6667701c8a221a8e963413c75a1c54d64a", + "proof": [ + "0xa7d254ee883dc59d29c71bda776408b45f1a9fd4667e5b182cba20b15133dd63", + "0x0a779255d0c195dadda520ff17d0441b331b3153c60f936355415d6a7402e2a2", + "0xdd3426b4e15a8f4fd901853093b9b07e6e42020adaa75277dc0147e59a0f8290", + "0x112c5e47475d52c67bc98d675e1056afe3e6623117b94507e29be69ebf20a435", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 171, + "address": "0x6ab880afd1e0c7786cc5d05f4fd9b17761768da8", + "amount": "3222627000000000000", + "nodeHash": "0xac1f333c32a9f1f183ee111e38850a588a9c36a995838837789c299a1c70abf8", + "proof": [ + "0xabfb51db5246b5da1b854bfce08a5b014e1d258a113a360f2b2ec1b4b3fcca1d", + "0x5df9cd5169dbf95d7d68d5b69af3c5d18986d7c4e40f77c9b1cda186475c9293", + "0xe503f1f66922357ad84f71a8cf0ee12029365883c7f8b27daa7791e6665bf8bb", + "0x77aac907780e1eb7ed08bc6fb93c7a472125d7cd0e8720e5b60c436f725d4b01", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 172, + "address": "0x8d461692cae92689beccb9fd6588e20401352a4d", + "amount": "2722110000000000000", + "nodeHash": "0x758f6cc6d03a96bca46d15666d7b0f61fc564708c1d74829f80daf001653538d", + "proof": [ + "0x759129628da533708c9f7b5e8e582a9864f153a980100a24e0e2d38438f65ad9", + "0x8499959fd748e0ddbc896423ff3fb98b83e44ed413aea1135eb52584d5155ebc", + "0x27e457d39207c5c9413452f66d1439674315c7ac031965edc3ef5983d37b3479", + "0xcd656b995bfbb006bd6cc7e0553e7b43a8a428952ce7e40550495ee52cd3d9f7", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 173, + "address": "0xab06ffd7e3c78050978e68a2c57f69b7c912b5f7", + "amount": "2730891000000000000", + "nodeHash": "0xbf491f133480a9267e7965111821b718d141d562b36eec29bd86aaccd3013652", + "proof": [ + "0xc1d897570302cd63543e34325ebb8776dcb2ba02098752124d98834c07c7595e", + "0x30a8d6141d357aabf88c9d3208d43fe9c32b95a9adfcb1b1afb67096fc29b113", + "0x77e041cf8797abfd574bda37e895763c1ccf70576b37398519e2c56b0a5698af", + "0x31d945c0d30504036c02fe1f0dc3952db23c29e3d87e59d1a06565ce6dae2924", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 174, + "address": "0x72c2c890625f7e2ec82a49a5b0d6438c36fc1bb5", + "amount": "2160126000000000000", + "nodeHash": "0x2bdbbdf569b7b1c61096d108033366e6beee97feca2f1af632e648de1806708e", + "proof": [ + "0x2c66bce03b69f9cd794a42b2e73b08bf4abeebd87301afc752e27e5f237899f0", + "0x9a71a9593eced4efb037a02294194c6d4be25a9294bb7b6c381fab74be5655e8", + "0x69dffad5ef567bc4c7cbd4fb5e9f41e131955b2a032a5ab4e7e97c5225575d1c", + "0x3f9c8e77affbca217664af93969c37b75db5f5e970d56dd0a2f7b374a9d8c12b", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 175, + "address": "0x8932d77775d64dff97444f28c699e6c3230de352", + "amount": "1800105000000000000", + "nodeHash": "0xf50625ae792acc8fb86b2ad484754f9199c736b94e005962fcd723c2fea10210", + "proof": [ + "0xf4a1752599644a28a1c5dc60ce7ddaeb0601488eaf5941f2e519eeafaa921daa", + "0xb344c8d5f1591e09ba72c6e9eb4cac6f5f0eb64224b0cb3fd7aca8321278d7bb", + "0xee9e134d432662feaf476361874386ca78329b4992de686bf58e678134ebeb7e", + "0x4374227d00e14e26a6659c58e26ba194704a9c4428299eefc28bfb5fbdf9f12a", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 176, + "address": "0x3ac892a09165516d98ec9c02b95ff840ab4badae", + "amount": "1642047000000000000", + "nodeHash": "0x8f66c59eccf7461e7efc0bd6840a7259af1683280149a757655e2e8e7e8ebf9e", + "proof": [ + "0x8f4998435932efef621b831d52e82a15081bea2be441c972e29d8c8692dccc12", + "0x42d66b4f5f6c05361d84ca6c8b2d790d21c1185a469b9aafbbc37c9533a5ea50", + "0x70320b540506f9453fa1e4f3dcb5f67f9cf8ea4c194e04076144154651c71b7c", + "0xcdc7c986bef6799a7b7aed7a38307c79b834bfb880319aecc678d4f1a76f3473", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 177, + "address": "0x473bbc06d7fdb7713d1ed334f8d8096cad6ec3f3", + "amount": "1633266000000000000", + "nodeHash": "0xc6f430587ff363e0814b5b3ac1d8eaac9b7715b6fd2868876bcdbb63596bbe89", + "proof": [ + "0xc5ee73508f61a2179ee2606a639adc0b7f591eb9fbf96bb9f765243f7fdf24cb", + "0x03ed36727f0091bce111caa9a4ee983b738b6a2826332d3b9e42364adb782605", + "0x888709e4e04365471499a37df65d5d44269f0a6015a49783f99d0e1487d8a89c", + "0x8f1e0bca0b330c8a6044d203531a4fe59749a3d5ebccd1a4a524fff2f807ad3a", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 178, + "address": "0xde46215e67d35972f4c880d59969dd08a4c9fa28", + "amount": "1598142000000000000", + "nodeHash": "0xef78514c9dd0d2aaf27dcb63d549443ba0d75de655a4c1270beec0bcd859a5b7", + "proof": [ + "0xf1b15a8ad5dd51cf1464a40a6681a13971c030c9e92281263a72148cb7a11d29", + "0x726d8f9fc12617a97a5d4c2f75780ba2ed90ed2b42e8695579d8ed25a0465553", + "0xbcb103468201a2c28e4f0287614283c7f71e3d3caccb0d84054ad77dd6f8268b", + "0x4374227d00e14e26a6659c58e26ba194704a9c4428299eefc28bfb5fbdf9f12a", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 179, + "address": "0xd785a0b9393f8280e1da72c81b8e6d18eda81db6", + "amount": "482955000000000000", + "nodeHash": "0xbbd0d88710ca501681d3d0921db16a6646187ca398a350d4e4196e577c4dd206", + "proof": [ + "0xba4c10258cd4f7c83441449d78cb56fff89d87b52952e87a4038bc8d55020eef", + "0x5e71ae954018bb5ce96f417374357bd0750722c2bb8b36e82158d6d40dfe5f11", + "0x4e2cf104325c508896b8c5cb332f174a17e5f8b29e2c5c58a1244e25d9ae856a", + "0x31d945c0d30504036c02fe1f0dc3952db23c29e3d87e59d1a06565ce6dae2924", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 180, + "address": "0x39dbdf7fe378a234020965897e7d9f33ea4bd6bf", + "amount": "360021000000000000", + "nodeHash": "0x90c8db9d3c49fe21f5e61ea613edab904dfb6c59d9f1f1970951c193e4e5cf2f", + "proof": [ + "0x9335d4352f53f68017507e57049d83d2fc9de8c00aef0ca0a94beefd0ef04004", + "0xb4afcd67762c70ed005e76f807784744c01447c350d058c6b746856486ad8e25", + "0xf511f28e9f90c67ec4ecd3d631874d8e9007bcf1f3e60d014b8f14f0091d93d8", + "0x0f67eff6e6b3718a60f4c58f3ee545014e05194beff2f3e75751393773068ffa", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 181, + "address": "0x28552ea8b232fc63d6ce69692d8a723efc5d4502", + "amount": "201963000000000000", + "nodeHash": "0xf1b15a8ad5dd51cf1464a40a6681a13971c030c9e92281263a72148cb7a11d29", + "proof": [ + "0xef78514c9dd0d2aaf27dcb63d549443ba0d75de655a4c1270beec0bcd859a5b7", + "0x726d8f9fc12617a97a5d4c2f75780ba2ed90ed2b42e8695579d8ed25a0465553", + "0xbcb103468201a2c28e4f0287614283c7f71e3d3caccb0d84054ad77dd6f8268b", + "0x4374227d00e14e26a6659c58e26ba194704a9c4428299eefc28bfb5fbdf9f12a", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 182, + "address": "0x94518da64ccc83cef38aa8eeb0c0e28dc181989d", + "amount": "122934000000000000", + "nodeHash": "0x377a520a3ce13c13e3990444ea926cd2a6910c03a34dac22acb1ce7f5fc5f094", + "proof": [ + "0x3989d9e21f1ec41b87170c422434d91efaa005618f10bd8a8d033fd87f25c958", + "0x684ccde3c00bb5dd1a27aa3862d5c93f331f6f8a2ee21db65d808f434d2ca075", + "0x9ee3fa7c938654616c76fc487fceeed95be956662b33deb34a3b9c277f0408f2", + "0xbd97d3672cdd0dce515b28377cdb7ddfc5d08d2a126f287bf0f31ad87296aab6", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 183, + "address": "0xb0dc0c7a91c34ac1698f9e7ec03104bdb3b42de3", + "amount": "11301147000000000000", + "nodeHash": "0xfbd8ee2f00013df211c070713ef96b94fe62f9884b624fa3ca3bf716e1c6a2df", + "proof": [ + "0xfcdb017efdd5c8d8ffa90985784ac74180360fac86bafc3b6b10012bf332ea50", + "0x1d413e56e8e75bc21af5f8c3f4af12a9dbaba6944992f27cd32a51871ec0c68f", + "0x8a190fca6b4bf18ee70ee088347d1d5ce46e23942d97e6d45cff623f33b56a8a", + "0x878b93f5379152f0d671b155b7a722b05eb555558bbfe56bfbb394a485c7952e", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 184, + "address": "0x338fbdea858e98f4294a73414ce0565a5b1cffba", + "amount": "4671492000000000000", + "nodeHash": "0x150f685876824ab1f0c932343d02ba6e213ebc8fb50ba220888d249912865a16", + "proof": [ + "0x14849fba436619635f0ad895c3543f4d954a2ac6b5d42ff49ccdd15b20a489d3", + "0x0847b7c7e50735d600f4d40579bf81fb8ccf98c72ba355cb02cf991318c71e5f", + "0x0e31324d8f7bdec33bc699b1ac845b07055f9836b7852f51934cbaca50f4967f", + "0x401b4840cfd7296b17a0bb231a8ee94c8c8fcd710eddb6978d46e0faf47d8eef", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 185, + "address": "0x54dd9c997ad6dbd6678852f94371741f07e1bdd0", + "amount": "4127070000000000000", + "nodeHash": "0x05c54c057df3df589f44d0434cb87b8d856f72b93afa90f10c44f255066b8278", + "proof": [ + "0x0557dfc6f86bfaef544d914e5bb9f82588c6fb84a4728f0ca4813de3427f7c69", + "0xd4293db5634e17e9208228d938b5ac8030969823042f29bb41c16ab47ef9da11", + "0x2ce668e3cdce60660255a67e7d4a5edf5412f5f39d06875e56a07a4c1d45097d", + "0x99927aaba68fa1c526c8be885fed2a583000a016f39e6210564017b20cc96842", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 186, + "address": "0x4c37700d949db46aa11a9f1627070f9af31f8011", + "amount": "3152379000000000000", + "nodeHash": "0xe4c9ba7f0beef2d8806d952216d1aa7eab18a6212c49edbe49e35bc3633c2dee", + "proof": [ + "0xe3fad270fad7dcfdc379b8a015d69d93b57af9c4d604ebe8ffd8f9392ae1783d", + "0x81e9bfc06a226d2b097763c0de33d2076e058f4bc5a20cab44c9a56f1cd18709", + "0xa88906f3d7535eaeabc785ccf9848d827c1660f4360f3f002aae67aa7a82fb27", + "0x2b7b07c087af29ef8f3779ca825e620e6a91fc1b8119a477cb9747b5bfcbe5c0", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 187, + "address": "0x327ae09aeffc9a276529b61870e896e733933801", + "amount": "1958163000000000000", + "nodeHash": "0x534176960099e6261929e0e7ebc89dccfe3c3aafd09143811a55fe303e5ad8fc", + "proof": [ + "0x535b3fb73668f45e89bc54a02b08c3f89376b64f771fdb9acf3dfc659c48f52b", + "0x996dca66a8f668fa948db0550dea9c2bdb3e8932ea7e7f42b7ac4b6c1037b078", + "0x4d72bdbb948be8a9d3a660d43eaefb5cccc88ede0e152cd8fed46d934ad5d0b2", + "0x1d51ca0e19aa543a6545b29cab8ac5f9483bb15f0c024a81b9aadfeb29b9f54f", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 188, + "address": "0x60acf8d95fd365122e56f414b2c13d9dc7742ad7", + "amount": "641013000000000000", + "nodeHash": "0xcf17c5c81df8c624386c8d1a3bcf56a80e7246b66c2b79220366bdbce9f0dbaa", + "proof": [ + "0xd12e0f00e9d13194cc4d6e0b2734e73c4771eeafdef8df295b5c736f8cf93ccb", + "0xe110e1b093e14e02b1806da636ee9a81490693e411fd80e7e49a4b2dc8130b5f", + "0x9b6bff949aa19fffa82a45966c6d082c7dd2c952298b7ab43268d0cfd3411865", + "0x049057c435e918361a347a9fe31a6f48819b657ce283c5390310bbc944753eca", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 189, + "address": "0xd4b6f9ce68c9432e32a48846c37c1549432add55", + "amount": "64672065000000000000", + "nodeHash": "0x57ffc5625b1e5e91776386096dbad49a252d862f4d0578dcbc62320a37565db1", + "proof": [ + "0x59ff47e6181cea66f99065285738b74ae463362cbd80118af58719e40bd1d8e0", + "0x42f6128e44456df432b83e004440e9b815662b4d71c563074a84c714196e8765", + "0xfb6b61993f883176c7796e3c8f60ed9e153ddf14ee510fb2e75e88c610562b34", + "0x58353663046a578dd1f9997dc38a4b7831c0a0b3796a5c26135ad8e0b4324c80", + "0x5ca42977a7a3007932221e3a783db390015025befc7d12da0e99d3d70bd472b3", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 190, + "address": "0x370a5275f69cb3c2e76cfe575cf7f846f906b34d", + "amount": "1211778000000000000", + "nodeHash": "0xf97172551ac48f31dc34c7faa7ac20b251f3c010dd57251dae01e6d45ac3355d", + "proof": [ + "0xf9f8f7332a1c3b10808062df4bfc99147fb247ccdbed1e8fbcd3764ed38e2a44", + "0xe5fb753a78a0a4d37c9306056e43f596a623353b7fb1f437620888121e923b71", + "0x16dcfeef35a8dc7ec586c8ce9af48cbdf90a8fae3fc96ce8f613094b4835f4d4", + "0x878b93f5379152f0d671b155b7a722b05eb555558bbfe56bfbb394a485c7952e", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 191, + "address": "0xdd2d4b9d36449e46f00b4a990d0f0b202498273b", + "amount": "316116000000000000", + "nodeHash": "0xd2ece52f60937e0182db66c1c28b3c6637175da901aa1290e88fea06f2e853b7", + "proof": [ + "0xd22143e34b5e3572d98fac263e8e1cd673d541c6cdfd33920a54fa76241c258d", + "0x8a76e1fb3209d3100160d93a8b364ec2fb9c867bcf0d0f69e0974cc4bc0aeb4f", + "0x888604db6f7d9a68ced6fff7cea8cdadeffaa0a505b7b4f019c88c28947bbb29", + "0x8c48e5104ea64540b9742e7e67c632558ee596ef39aef3dabc5a750b5a0d8de1", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 192, + "address": "0xdcd0f870018032c72a782de9e3d9ae75d216d75c", + "amount": "140496000000000000", + "nodeHash": "0xd196066af11605f4f68453be577019ccdb951d8dd6cba340945d1e2dc97cd89b", + "proof": [ + "0xd1570452b5e123e733cb8498264e9b631401fe20423f2ad81e0782d2ecd8e466", + "0x3d47719c3dc074ed25e959f607834f76c8813b873014b57b5ceb1fa448d1054e", + "0x9b6bff949aa19fffa82a45966c6d082c7dd2c952298b7ab43268d0cfd3411865", + "0x049057c435e918361a347a9fe31a6f48819b657ce283c5390310bbc944753eca", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 193, + "address": "0x93006f9dc69be8b9f1feaab318f3b4bb3d32f2e8", + "amount": "15199911000000000000", + "nodeHash": "0xd1570452b5e123e733cb8498264e9b631401fe20423f2ad81e0782d2ecd8e466", + "proof": [ + "0xd196066af11605f4f68453be577019ccdb951d8dd6cba340945d1e2dc97cd89b", + "0x3d47719c3dc074ed25e959f607834f76c8813b873014b57b5ceb1fa448d1054e", + "0x9b6bff949aa19fffa82a45966c6d082c7dd2c952298b7ab43268d0cfd3411865", + "0x049057c435e918361a347a9fe31a6f48819b657ce283c5390310bbc944753eca", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 194, + "address": "0x23382af512fa63e1daad26ed8091be91ad1f4597", + "amount": "20011899000000000000", + "nodeHash": "0xc8b16b4a73c9aa1f18eb7f5e071965d88bd3bb7bd325d1e564e004c2c156116b", + "proof": [ + "0xca812a1ab8ce48078e163742fb524693a7a967f1fa443013d4e780c598584af4", + "0x41db7acbb5b9dc0f76b35d44fd907171992c817f0690ffe47a195daa64447321", + "0x1de190c06efaa371a311ee112f45007771a2745d514c4f0209909371267989c1", + "0x8f1e0bca0b330c8a6044d203531a4fe59749a3d5ebccd1a4a524fff2f807ad3a", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 195, + "address": "0x25f8cc6c8c4af1076a38872c8b110874dcb144a8", + "amount": "5461782000000000000", + "nodeHash": "0x45cb3d77485ab5b160d42585569b47dfcbccc969cbd0a767141b5ae8b3a9e939", + "proof": [ + "0x4796a5c8ffefcaa6037117160662afa92c8cabe99db4f9bfeede846860a35fed", + "0x87878a52868259f7af40a9350822b0c16cce4921d6b16db57a1de439bb28e528", + "0x69213dfbff5f3d0347b6bc4ea800818dd36cb94d6fa797a3ceb3dbac048e242c", + "0xf5206d1a8eea30d8766d8236039d92db78f10d58c4069671da4386cc649fb9cb", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 196, + "address": "0x5b33443efbd5d2bc5149f7ca81922f4ed5d6397b", + "amount": "4109508000000000000", + "nodeHash": "0xa01acdde1ca8ae43a98e53187d2cb65849688fd99edfd4441b50afb8a988bfc3", + "proof": [ + "0x9fd94db8ff650eaf61c6c7b5f1f6258158dd33a09ac498e2a8b190169bf083e4", + "0xc6a6bec0cd3c286e22a1e4f95151526f0f85f9a7f8dc72e4af2e287a777cca81", + "0x99915e59da35c21cdd6c6166d0106f8d8f42d9217c368ff7212a04abf0f34299", + "0xfc494fd4d56122448384ef08f6ebb78947a312c2e1369042b28819274e2acf66", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 197, + "address": "0xd7c9fa90edb7b40c34d6de1d09c750146ba2e8eb", + "amount": "3266532000000000000", + "nodeHash": "0xc317c3194d51f1f55a13515836500a5cbf2838a8e1d65616c50ac259c105b056", + "proof": [ + "0xc499f325162a45963553fe1831882aaad5fcf7d4783d291f61bed6245535bf83", + "0x5c8acf1eafde10dffa8fecee651fda48d6a7e733b5e06d15c14775c96f07d392", + "0x888709e4e04365471499a37df65d5d44269f0a6015a49783f99d0e1487d8a89c", + "0x8f1e0bca0b330c8a6044d203531a4fe59749a3d5ebccd1a4a524fff2f807ad3a", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 198, + "address": "0x771e3d20dd07df1295b9d4ebb149bca92ce2d923", + "amount": "4285128000000000000", + "nodeHash": "0x36e67cb55c255c0ebad42621d5fe2e9e0d8bed9f687b5ad4e6af2259248a356f", + "proof": [ + "0x36fc90f30c9be3cade5b44aa58622fa1084a838045f62a71c55e5cfa68ae3dc5", + "0x33ca0790f218c81d7e3d7c63e0a28f672d57816fc2ec41e5bc1c86ac94cbacc3", + "0xffd4f9d79518701e9fbb847603851d8bcb3af375c38eb6e9de63aa69d5b362e3", + "0xb149db133f2de53fa1c5bfb7817947aa4b91fba91a7f427e3cebc44a51c0bb8a", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 199, + "address": "0xc4754f7da715aa37c7075d8400713378aaaf4f30", + "amount": "2204031000000000000", + "nodeHash": "0xec33e4324fae8d2e02f0fca6fae6df0f00962be817b4aa12f8dc1edc37c575a4", + "proof": [ + "0xeda7230e3380aa4f5a7571061a6e49c07b4dd87a470dd9e0fabe2b740f94977d", + "0x7ba3d59ab6597fb5e7133104a2335785257c5e8d76f1678f50ccd466bfb67472", + "0x31671f7e335400af5b05a1bb5bdfb79516d12b6b57500a1d7aa1e53bef10935e", + "0x3f59b8cdb34f4bc3b9c931471fa7d84b69826722d8401478e0c9afbc1f56fa16", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 200, + "address": "0xa29fddec9c36bdd680d3f5867735ff8949a7f15d", + "amount": "772728000000000000", + "nodeHash": "0xaeebd41522c521b42f2379e13a34069f5715c75f6c90297da466743d08540f30", + "proof": [ + "0xae8fd7743d5f2245b8bd0572e464ab847cd16490da7d05c7eeeb7e183c259a0b", + "0x8ce3efbd5f46277dce9ed07d7992e9cd790ef01d9fa2817bec4de9f0cce83cd1", + "0x7a5ff3f19b447c6cd8d97470d640993dac0e19acd049d11248b6c33752696a5e", + "0xc9285e31e87d3c235fc0dc4ab5c5978891a414a32a26c69b43d5834adf7fea91", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 201, + "address": "0xad549c7fcf1bd3e57bca464d9dce0a0d561599d1", + "amount": "526860000000000000", + "nodeHash": "0xabc3de0fb2fa4a248eaab7a56227e1b0bde4a2b982205bb5ca52304807d754fc", + "proof": [ + "0xabde5cb713847f282e123d47feb2d1e774c005e4825ea22d94cb69c1bfad44dd", + "0x7c790d3433f68cde7a1ae87e232768dea7bf38d01e49eba530eedab0441c541a", + "0xe503f1f66922357ad84f71a8cf0ee12029365883c7f8b27daa7791e6665bf8bb", + "0x77aac907780e1eb7ed08bc6fb93c7a472125d7cd0e8720e5b60c436f725d4b01", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 202, + "address": "0x0f7c548c3c100e1ad6a1f092d95884cc4dcef4bb", + "amount": "474174000000000000", + "nodeHash": "0x9adae8c124ab6de7ff841311465d2ff84c248325051f5ef34602c32136e918b7", + "proof": [ + "0x99b5ccafc49e01f50adb44624c8d6fdd007b117d1a0cac56a81e85c0061f3a1c", + "0x76c5f38de78bdd5e40676bc0aef50d302398699cc45fd2ca3620abcb86720d6f", + "0x80d3320efcf13447783dc7c21219a96e1b9acfb649c33c72903af272ff3b6f25", + "0x0f67eff6e6b3718a60f4c58f3ee545014e05194beff2f3e75751393773068ffa", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 203, + "address": "0x56c494ec0ceeef43dd000b812dacf965bb36a6cd", + "amount": "43905000000000000", + "nodeHash": "0x82c5a7b709a7d5b49d7420925c9c98025cb4a7345edcfa5819e9bc6b001eb6a6", + "proof": [ + "0x8195ff1d91fca20ad1a7d2dd75d0196da290593306ccca624270a030a95f8382", + "0xb2095b3b5602ca26b450a6c509aff07f2b29d60fa605c1ee0f574eb502fc5715", + "0x9ee727d3ed6f58402a106625a4d4fd294c3e9046de7e06e40540f5ca498d0432", + "0xde8a996044401b7617b89b4add1c4cfe27060c1b474a658abc3cbe0c0ca542cf", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 204, + "address": "0x0b39a46d9fe2f26ffe03c5f32fdf19541e1e92c8", + "amount": "29916867000000000000", + "nodeHash": "0xf7b62fd09c424bd542e2166f7a4d3e8f7b31ac97ccb8a1d6cd3a1b2826f1f6e2", + "proof": [ + "0xf7a3e9eaee0c02f3b74f7d9e096e9e790b89a17c0ac2b1426aee72df30d1c740", + "0xd7367cecf0b43d01d848ccadc0bf5a3c2be0404e254afa404a6db5cd55530bc6", + "0xee9e134d432662feaf476361874386ca78329b4992de686bf58e678134ebeb7e", + "0x4374227d00e14e26a6659c58e26ba194704a9c4428299eefc28bfb5fbdf9f12a", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 205, + "address": "0x499f39d4861f214808376c890aa7cf6e424fb05b", + "amount": "6252072000000000000", + "nodeHash": "0xea19c735384b74ceaeff0ed09ed5864e3cabab8eb77e59111047a450b2d8be0d", + "proof": [ + "0xe9ac2526e391f9357b4cacff1be1b29233a2ff71f290d4133ad33619f1137ccb", + "0xa72a53caefe3e0fa4bffc908960ab58091b31b55fa12f7505dc4941dd7207cab", + "0x31671f7e335400af5b05a1bb5bdfb79516d12b6b57500a1d7aa1e53bef10935e", + "0x3f59b8cdb34f4bc3b9c931471fa7d84b69826722d8401478e0c9afbc1f56fa16", + "0xb2fb6f40206d1079d3da7e8f0573cdd46be09007ef56c630e16afaf6d9225afd", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 206, + "address": "0x2ece0f323384e9076383cecaa479eb7ca5376126", + "amount": "4908579000000000000", + "nodeHash": "0xa2b407407882c46166ecdb12eea3eadb3420b4854c43d4fa116d201a92ba8e88", + "proof": [ + "0xa30f985f84eb0b752c29e18cdbd527a5cd9b0ae58ec33e97b60103c13bcfde80", + "0xe03539dfb7faeff1c7195d7af6d45594dd78bbd31c8bc23678ea9dbd8d2cbd92", + "0x2a04f687325c7538cb6f084fc767c5ed72dac0dad6a28ef23986fbcea61df2e0", + "0x112c5e47475d52c67bc98d675e1056afe3e6623117b94507e29be69ebf20a435", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 207, + "address": "0xc30b4930620431f0239dc2cbc478c51eaecaa2ae", + "amount": "2792358000000000000", + "nodeHash": "0xa81db51064f0b40811706f68450f1cd023feba364708c485cecc0c07875a2793", + "proof": [ + "0xa9ba4a46fd408763049e011ffaa76e554eb516078fb35be6b781b1034dce27c8", + "0xa3acbd14a7e5ea26e2c9647588d9688a285ddac6f54c1aa5444d27e99ab71c7a", + "0xce2d66202d26fe1faf88499baedd97a1f2f5dd90211b19a3d4f77e9b88291311", + "0x77aac907780e1eb7ed08bc6fb93c7a472125d7cd0e8720e5b60c436f725d4b01", + "0x7d8c4c5e16268307c95a89750e6bfa0c3e4efb4cdfb37ec0cc17e2c512a74bb3", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 208, + "address": "0x42a60d2f2ffa2150c568010a8d425f0aad284fd2", + "amount": "2757234000000000000", + "nodeHash": "0xb5947c5c069ce6bc94929167d4e0e254f167ed9d0e115dd8f7747b27b1c02582", + "proof": [ + "0xb80487c07af4b3e1e40fd67b513cd4dbd21b7964639b308edf5ebbd8d9f99384", + "0x7c7373ea5af83cdcb86eb83b2d2c45f847f5c805bfd7adaf7c31435ac6c016c7", + "0x28f06e02ae1339fc54b9c964d1b5ad118e18997a61fcf36511d31cdf927cc508", + "0xc9285e31e87d3c235fc0dc4ab5c5978891a414a32a26c69b43d5834adf7fea91", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 209, + "address": "0x6d38939de57c10b880f1eb2227da45ff9174a095", + "amount": "1492770000000000000", + "nodeHash": "0x3605a2911eb719fc2c22fec90fdfc050fe189772ccb0eccd94d8ae89d388a9b5", + "proof": [ + "0x363dc5ea5748610a62fcec1e6cae5d777feaab525d585dc9a19f84613f9d315c", + "0x3d8c7b4e9111b1fcce37580d6352360fd2a748e27c821dcb9e63c6bf24203c4e", + "0xf7478ef32da8f4830df5edcd72303756fd6f171973be2e49aea5212970a2273a", + "0xb149db133f2de53fa1c5bfb7817947aa4b91fba91a7f427e3cebc44a51c0bb8a", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 210, + "address": "0x680022389e2cb129209c2042940075ca53a10707", + "amount": "1141530000000000000", + "nodeHash": "0x36625ac2ced62938db2f9272088a86f060e6e48c84da114a64b7b112ecb2fde5", + "proof": [ + "0x36cc998d1c4d9b08b2c664e30e487113e495b4dbb58b6bfb29e32719bfad94e6", + "0xa95d27bf8afffd771be96a8e8f1a9a49699cac5ce1b246a2a197b0b6bb02a63b", + "0xffd4f9d79518701e9fbb847603851d8bcb3af375c38eb6e9de63aa69d5b362e3", + "0xb149db133f2de53fa1c5bfb7817947aa4b91fba91a7f427e3cebc44a51c0bb8a", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 211, + "address": "0xb4d502361a3c6f823eeb9a99af09e110382206ee", + "amount": "913224000000000000", + "nodeHash": "0x9bfd0dd73cd00659f7ffaa43583ece659643e456c1727fa5104630664e712ac8", + "proof": [ + "0x9cb73a7a4515d09379322ea6fc70fbc2445b2f2bc58357f6db2a8fd7513b7fd0", + "0x20860ec6f1a2509db3c3cfcbfa60b75c123224cbed4f95de237c94a2e711250d", + "0xcfb31d36e8200eb045fe23af2958fcf64e7c29d370930d2f986db9135dba31ab", + "0xfc494fd4d56122448384ef08f6ebb78947a312c2e1369042b28819274e2acf66", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 212, + "address": "0x77d05478901453f93fafcf9aa4814e54e585d8ef", + "amount": "1642047000000000000", + "nodeHash": "0x9335d4352f53f68017507e57049d83d2fc9de8c00aef0ca0a94beefd0ef04004", + "proof": [ + "0x90c8db9d3c49fe21f5e61ea613edab904dfb6c59d9f1f1970951c193e4e5cf2f", + "0xb4afcd67762c70ed005e76f807784744c01447c350d058c6b746856486ad8e25", + "0xf511f28e9f90c67ec4ecd3d631874d8e9007bcf1f3e60d014b8f14f0091d93d8", + "0x0f67eff6e6b3718a60f4c58f3ee545014e05194beff2f3e75751393773068ffa", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 213, + "address": "0xa0b343ab316cbd95075a8d9797732733de845852", + "amount": "2309403000000000000", + "nodeHash": "0x9d69517f0bdac5765aa462977f1daa57f12b2c951662f10f7caaad817798d0f1", + "proof": [ + "0x9dc8b99fe2e2d355ac61fc081db3139fb08514e518042cb8a8b99a2e7e16a287", + "0x05232f0893f1accd6d9dff4e6b2344dafa4d5909c16b85302885553a492fdc60", + "0xcfb31d36e8200eb045fe23af2958fcf64e7c29d370930d2f986db9135dba31ab", + "0xfc494fd4d56122448384ef08f6ebb78947a312c2e1369042b28819274e2acf66", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 214, + "address": "0x69344717556c64dc49a2ba36267a04efacf34d27", + "amount": "676137000000000000", + "nodeHash": "0xca812a1ab8ce48078e163742fb524693a7a967f1fa443013d4e780c598584af4", + "proof": [ + "0xc8b16b4a73c9aa1f18eb7f5e071965d88bd3bb7bd325d1e564e004c2c156116b", + "0x41db7acbb5b9dc0f76b35d44fd907171992c817f0690ffe47a195daa64447321", + "0x1de190c06efaa371a311ee112f45007771a2745d514c4f0209909371267989c1", + "0x8f1e0bca0b330c8a6044d203531a4fe59749a3d5ebccd1a4a524fff2f807ad3a", + "0x8289fde233b46c09b9bccb4ad13b754dc5b3a0e40c19d0e02132363973bbbfb0", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 215, + "address": "0xa963d0869966d022084b7f70228cecdf9b5e5a87", + "amount": "667356000000000000", + "nodeHash": "0x67c5fa2871cdee9ed4bd3270cabc748d221a0d100cc4ac7567fa3999724e934b", + "proof": [ + "0x6928c1c641189630d392ea596a92550e2bf1488854a0cec7890a0684cfbb7c34", + "0x6afcfcf0b1dcdc884ededa3dfdfe729c0fcc28f2e1d1694f8118b2a10f1fefc6", + "0xe2c4b5e85f6c1a15c4a2e920b160b9db8f2be4d562b8fce0fcb9b0249335bb14", + "0xbef3adfc06e9c1b99f0036e6bd56baa2f939d20e0f53ee8ccdaf789586255173", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 216, + "address": "0x94228872bb16cbcdfe010c42a8e456d15b366bf1", + "amount": "439050000000000000", + "nodeHash": "0x964e44634a3453cfc476a9b587e180460b47111fa94c8745d548e392aa3aca28", + "proof": [ + "0x973c4864a71491b9e53f89770a198ec0d05aa7b92dd26341f787483a3ac31502", + "0xf0061c6c70cfa67cbb6c3a194e6e9f8de3e5143a763b91f5c869038b912d41ba", + "0xf511f28e9f90c67ec4ecd3d631874d8e9007bcf1f3e60d014b8f14f0091d93d8", + "0x0f67eff6e6b3718a60f4c58f3ee545014e05194beff2f3e75751393773068ffa", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 217, + "address": "0x9ff10612af2b7ab14dcdaeb2f8e41ab6f54d020d", + "amount": "403926000000000000", + "nodeHash": "0xe00a47773d6bc089d883cf598468724432fe5bfab494443b0c1fdee3e601e019", + "proof": [ + "0xe31cdbefd11914592fb72c1719b8a2eeca9742bcf1b14bcc9f48a5dad77bbfd9", + "0x55bca0a62ba8e309211482c13a6952fd4e7da03cf08320063b55d3568660321d", + "0xa88906f3d7535eaeabc785ccf9848d827c1660f4360f3f002aae67aa7a82fb27", + "0x2b7b07c087af29ef8f3779ca825e620e6a91fc1b8119a477cb9747b5bfcbe5c0", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 218, + "address": "0x7ef07db2ff69d77acc83436f5b3fc1820562ed94", + "amount": "351240000000000000", + "nodeHash": "0xf9f8f7332a1c3b10808062df4bfc99147fb247ccdbed1e8fbcd3764ed38e2a44", + "proof": [ + "0xf97172551ac48f31dc34c7faa7ac20b251f3c010dd57251dae01e6d45ac3355d", + "0xe5fb753a78a0a4d37c9306056e43f596a623353b7fb1f437620888121e923b71", + "0x16dcfeef35a8dc7ec586c8ce9af48cbdf90a8fae3fc96ce8f613094b4835f4d4", + "0x878b93f5379152f0d671b155b7a722b05eb555558bbfe56bfbb394a485c7952e", + "0xfd2c8b3ec2f974f1959f77d6c629af3763d95c5b8a0ef0caee3f76a86d67309c", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 219, + "address": "0x17da7237393f241ec405833d409116aa4da44f75", + "amount": "43905000000000000", + "nodeHash": "0x2ac56981bea8ffe5d21014b5c9e8a9e2cfa92a125d2205c9bc5e9b4cc958113d", + "proof": [ + "0x2a6c8939f84daf5408b386090794d0c9255a6fb47616e5df7eb8295746808075", + "0xd11a8de1fe3f2ab1e946fd84c0c6b26e8c263e7e6e6825020cd776a0a8a7b944", + "0x8b17088d53d8cd82f15cd19fd878ceb428963b30615a65fbe1afc21c9fac39d5", + "0x1d9b0c09a3cb63729974fa7589d3e021eba7ef36ed2ec96be004ae76e40e4456", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 220, + "address": "0x896022620ca215a0e5c8510095f194f46c3470db", + "amount": "0", + "nodeHash": "0xd83f6823acf1794bb6b48833a4b3a57df69d44736e90c323b686dbf685805e28", + "proof": [ + "0xd8659b67883b3dc6fd26affeadc648a8703ce7bbe1d8fae3f7b3b0ff17bc6a7f", + "0xd87f37ad469643ca9493df77ad0ee2a366f60ed60511208669a222864a3cd4ae", + "0x888604db6f7d9a68ced6fff7cea8cdadeffaa0a505b7b4f019c88c28947bbb29", + "0x8c48e5104ea64540b9742e7e67c632558ee596ef39aef3dabc5a750b5a0d8de1", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 221, + "address": "0xa81e70d7b0678d21c635a4c81e0cf47fb7c8ede4", + "amount": "248976474000000000000", + "nodeHash": "0x0b83b157c8d22b101f2b07161fb9b32290ffc22155dff1286d53d4b1d1750c6c", + "proof": [ + "0x0acc83080d8254fa3e8153373c34cc15d5685e352135766b17f1c0a70ed70674", + "0xc75e7cc56c9524dc75b3c19ca4e99a17e7dc9aaba3d423ffdbeed59b260781c5", + "0xc7e6a378be7722288c6d0e53e9c3c2681b948dc95aaa85fc9e61e4385b30260f", + "0x99927aaba68fa1c526c8be885fed2a583000a016f39e6210564017b20cc96842", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 222, + "address": "0xe2e4f2a725e42d0f0ef6291f46c430f963482001", + "amount": "29152920000000000000", + "nodeHash": "0x2e43d884f380a5b0e9ebebdeaecd5db76d67c4b710b58ec673c3cd33676589bb", + "proof": [ + "0x2ec1369488455fb59365c2659324b21fc54f4aa1fd45dfb61fb4c324ad8ede5f", + "0x3a7ea8bbab2341f461c79eee187d8029cc9ca76b1fa8383cb06e5abcab881cf2", + "0xae70dc4f1661eb4cd5ce6ce92d37555b22131bab4d1e2299a12a96bb693592b8", + "0x3f9c8e77affbca217664af93969c37b75db5f5e970d56dd0a2f7b374a9d8c12b", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 223, + "address": "0xf69ea6646cf682262e84cd7c67133eac59cef07b", + "amount": "10414266000000000000", + "nodeHash": "0xbe6cb0716b81670ab26f75d178cd1015ee51c8799507680af2515aab4c8e9cca", + "proof": [ + "0xbea10368ac8a59ff0a3e7effaf767fe9e18c16e729401f698f3fe46bb7faf528", + "0x437ec4f889e5185767eaa32b6e90d9e4854979b2d53677d64186fc7b985c23ef", + "0x77e041cf8797abfd574bda37e895763c1ccf70576b37398519e2c56b0a5698af", + "0x31d945c0d30504036c02fe1f0dc3952db23c29e3d87e59d1a06565ce6dae2924", + "0xd52d90d13af95ae0f4fafa1b11684a27120bebf82104d42157b0821ae4eec33a", + "0x4ad994679426be20bc1564228147f0fd17df77375fd0de677fdde6bad38118e7", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 224, + "address": "0xd56ba2c35537c4f1b926a02dba0e46241a18c66f", + "amount": "526860000000000000", + "nodeHash": "0x9ee70147ff673fb436fff590cc88df68d5d95796b8c4bf1be90a3243b857f055", + "proof": [ + "0x9fc2cb5ba4a2610f57fa85ea806e2f53a82cbb1e20149dfabb3bf6421e59f897", + "0xc5fbf020a95ce99d05b527b5916691f9495abad0909f9b5f934b7695f1bb149c", + "0x99915e59da35c21cdd6c6166d0106f8d8f42d9217c368ff7212a04abf0f34299", + "0xfc494fd4d56122448384ef08f6ebb78947a312c2e1369042b28819274e2acf66", + "0xd00e0bf9c90f209a4c17dcd53d7fc5446d4011a9e309487a22af8a33de9dc8a5", + "0xbd10c69513d8f01777346bf3bb2505e6a71b9509545ad1e3c5d7b7b07af1fecc", + "0x962821492d12ea53b9888e0404397f2472aade15ffc96e8b3d9008b2ef87d410", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 225, + "address": "0x7ede3c6fe8e68e420c23ada6eb9ce9fe99f74241", + "amount": "500517000000000000", + "nodeHash": "0x0ea0a7b2bfef2b32b474f642d4c1e7e75bc49b47317a9259954a1172792ff210", + "proof": [ + "0x0df65bc43b277750168597930bdff98b96ca64526648ee2ffa3d9caf6faeab1d", + "0xbb473b4b3dcc2b076983245b5d9817b98cc51377bc166b349b1b249646ad24af", + "0x27f5ba055019acd91796b1eb1f12fbaa9b667c42bb565c2b0aab0809e3752977", + "0x3568ae0dd2327f26f3bd4c1af82d9a7d8132e50855cdc1c20f276776264d6ab5", + "0xd77ab175955cd5a872c98a7b66d64935386c38d72f15ba90ae3172d78e0b5a2f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 226, + "address": "0xcc0333bcb5e0de0c14fc42a1bdc85efcd36612cc", + "amount": "447831000000000000", + "nodeHash": "0x75921cf31c0761691e6f89d99f3f1bb25513f3bb3450e1d6ca5ca7cc7e0877a9", + "proof": [ + "0x789db8691e99b510e2ac94745a6ffe9eda19e78ba0eecfe1795f6c66de8328f4", + "0x46eabe7b9e80724de8e4828d9bbbdf1686f155e8d2e47ba37f46a799acb9eb23", + "0xa2954bce93ae1f34dedb0c78d5f07a45d696775993a5716f85f184e6114bf588", + "0xe92595020625bd816794a0573140fc5d9e5dda15bf5439f01004d872a3811f20", + "0xa76bc382db446b55864e764fe44426a0c96077266b720ac099025bf40b2a3642", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 227, + "address": "0x1f41a778350f819a686625b506545242d1fcf418", + "amount": "430269000000000000", + "nodeHash": "0x14849fba436619635f0ad895c3543f4d954a2ac6b5d42ff49ccdd15b20a489d3", + "proof": [ + "0x150f685876824ab1f0c932343d02ba6e213ebc8fb50ba220888d249912865a16", + "0x0847b7c7e50735d600f4d40579bf81fb8ccf98c72ba355cb02cf991318c71e5f", + "0x0e31324d8f7bdec33bc699b1ac845b07055f9836b7852f51934cbaca50f4967f", + "0x401b4840cfd7296b17a0bb231a8ee94c8c8fcd710eddb6978d46e0faf47d8eef", + "0x6feb023ee98bd945960c6da9f3c47aca426dd74bca9f1b64d19820363c04516f", + "0x49f275d51e22b4c5c46306038be494410946253f193b983ba316fbff9be2c762", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 228, + "address": "0x940e4d88158f5ea1610c93c11354a9c9f7f00988", + "amount": "298554000000000000", + "nodeHash": "0x4c9f39fe6d842887da96928f2a1b087800c0de3e0429032c22b15ca3295c33fb", + "proof": [ + "0x4ba85cfc345b6efcd2457dcd6719f9a59627351a217ca4068ea95d24735029f7", + "0x76f40ebe9f7ef73c61c2a6c90e1bb3a013223f86882771c6c18a4e59e395adaf", + "0x628f4d9731d41648d4ca18d657beca29b7b92de3ff4b8838cbee00e38ccc4ad0", + "0xf5206d1a8eea30d8766d8236039d92db78f10d58c4069671da4386cc649fb9cb", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 229, + "address": "0x23e9fd8572e5ad4651c0a0a5436d872810dfaf02", + "amount": "219525000000000000", + "nodeHash": "0x84c3bbd23b7bc4d625020aa2c347b902fbb26bceff9845a02c01ae4cd3177e17", + "proof": [ + "0x83413dbaaa6fcafe53c43c215e9cb86303df838a00400383b08d573e2e9bf722", + "0xee1183279d3d281f008fbdf3cb35a606ae474e0a5673450c7a56c2a43c57a11b", + "0x9ee727d3ed6f58402a106625a4d4fd294c3e9046de7e06e40540f5ca498d0432", + "0xde8a996044401b7617b89b4add1c4cfe27060c1b474a658abc3cbe0c0ca542cf", + "0x3678de2cef837575285fa09b285c2e7e60012f10c7defd858022faf50310cfa4", + "0xd4dd4c4b66ff84ec699354007e2e1d3169918cbf23ecb6b0198ca92d3675b5d6", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 230, + "address": "0x4a1f8cd64f79ef1daa4c939a3bc23dfb26bc0503", + "amount": "131715000000000000", + "nodeHash": "0x4ce75cc58a99bc1319e030c00e419878e47a86d7ece12bb50d2d95d69c4f8741", + "proof": [ + "0x4e717ffed58d85120beea2e8265d9d846b255d8c14dbeb7c14eba03903300b0a", + "0x433d42ba2b9b347e7209cfe76c6616bcc97a25e92c1790cb09f81ee6a08930ef", + "0x628f4d9731d41648d4ca18d657beca29b7b92de3ff4b8838cbee00e38ccc4ad0", + "0xf5206d1a8eea30d8766d8236039d92db78f10d58c4069671da4386cc649fb9cb", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 231, + "address": "0x68575571e75d2cfa4222e0f8e7053f056eb91d6c", + "amount": "96591000000000000", + "nodeHash": "0x44970fa0ec9e88801114883c8a934c4868698ee159c77e3db81aa8bf47e02a54", + "proof": [ + "0x444a8991af4dd46166269c36b09067fffec991ec9542f68ca10c6d029c486e0a", + "0xd9c25e45e318bee712d957043e4a6a8c3590baea7debeb06b8bd2ee9c638ee1b", + "0x69213dfbff5f3d0347b6bc4ea800818dd36cb94d6fa797a3ceb3dbac048e242c", + "0xf5206d1a8eea30d8766d8236039d92db78f10d58c4069671da4386cc649fb9cb", + "0x7698f5e5048e68f87a1cf0ea54233e22b648d3b0b3fa4a5dab3fffb1c1b90859", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 232, + "address": "0xdf570f0be90c7b9d8934c898df7b475ee1e47225", + "amount": "96591000000000000", + "nodeHash": "0xd8659b67883b3dc6fd26affeadc648a8703ce7bbe1d8fae3f7b3b0ff17bc6a7f", + "proof": [ + "0xd83f6823acf1794bb6b48833a4b3a57df69d44736e90c323b686dbf685805e28", + "0xd87f37ad469643ca9493df77ad0ee2a366f60ed60511208669a222864a3cd4ae", + "0x888604db6f7d9a68ced6fff7cea8cdadeffaa0a505b7b4f019c88c28947bbb29", + "0x8c48e5104ea64540b9742e7e67c632558ee596ef39aef3dabc5a750b5a0d8de1", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + }, + { + "index": 233, + "address": "0xa1cb4468f529f51296180d148f4764adde8563a3", + "amount": "35124000000000000", + "nodeHash": "0x2c66bce03b69f9cd794a42b2e73b08bf4abeebd87301afc752e27e5f237899f0", + "proof": [ + "0x2bdbbdf569b7b1c61096d108033366e6beee97feca2f1af632e648de1806708e", + "0x9a71a9593eced4efb037a02294194c6d4be25a9294bb7b6c381fab74be5655e8", + "0x69dffad5ef567bc4c7cbd4fb5e9f41e131955b2a032a5ab4e7e97c5225575d1c", + "0x3f9c8e77affbca217664af93969c37b75db5f5e970d56dd0a2f7b374a9d8c12b", + "0x00914103e9c2691c552fb1e7d8b870aa201b5b49cda5437d73729b90ca1ae6e9", + "0x0c2c4b1b8c5e128a1ca8bbb23f825d27a09ea6aa7b53310c01b6eb61fbacaa13", + "0xbe7a191d2d2c2a179d1bb43d21cf05cf95b0af5d36606de1754da8abe4e0d5bb", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 234, + "address": "0xf8e255accfba492512c11a780e5eac2d88dc3d06", + "amount": "0", + "nodeHash": "0x5fbc134ba0a8f0a9e29246f8673fe47d345a564648b43da05a3e61281604b182", + "proof": [ + "0x5f42b29d1228f686214b091d51ec7c472dae6e1f0bc4f5263db1a7b1262a3c68", + "0x9e4385a14841fcac5ed1fd7854f7c22ab1439c72f3b94a5bca07367fc805d646", + "0x49caa557cb6111a0eda5b1f13ecfa94fb09fa036e054a1faeac9f3ba92b8af6b", + "0xf97c70e9a61a1af1e7abe2d3dc3538c1ed7c5556d96b05a481e88adc38bbb1c3", + "0x3d27a9530a64fb46f77a5261f57d8c866ff669ff6565721d312333cde9d406c2", + "0xe6a76d29b99f3536aab9f3543d85226925d87879490419e9efdbbacdea274c53", + "0x22c7e6b1f2b4658f3812358628896fe566aa06127fcac95729095e91507360af", + "0x91cd1e43a3486470eb8b8fa8f8d3ca9eb245a5624f39316f3fca95a23f53292a" + ] + }, + { + "index": 235, + "address": "0x110f7259016e4a72e07523d79a8de79639f2f77e", + "amount": "79029000000000000", + "nodeHash": "0xe31cdbefd11914592fb72c1719b8a2eeca9742bcf1b14bcc9f48a5dad77bbfd9", + "proof": [ + "0xe00a47773d6bc089d883cf598468724432fe5bfab494443b0c1fdee3e601e019", + "0x55bca0a62ba8e309211482c13a6952fd4e7da03cf08320063b55d3568660321d", + "0xa88906f3d7535eaeabc785ccf9848d827c1660f4360f3f002aae67aa7a82fb27", + "0x2b7b07c087af29ef8f3779ca825e620e6a91fc1b8119a477cb9747b5bfcbe5c0", + "0x2b4c16a556799a23a31a8eef93511efa5b1c2d6ee0f31fcb36626dc96c432f5d", + "0x406ba4ed96bed0b0984970a5a822415290736db6832f82ed1a407285cdefae46", + "0x8c1ceb89ae262e6bd5d2980aa81f09b7566cfca9a6e969c5481ecdaa825366eb", + "0x779a80e535423e0a18f154234cd33516eafc14bb2fea1ddbcb2fc8a88561991d" + ] + } + ] +} diff --git a/protocol/package.json b/protocol/package.json new file mode 100644 index 0000000..3d7d28e --- /dev/null +++ b/protocol/package.json @@ -0,0 +1,26 @@ +{ + "name": "stake-dao-protocol", + "devDependencies": { + "@commitlint/cli": "^11.0.0", + "@commitlint/config-conventional": "^11.0.0", + "ethlint": "^1.2.5", + "husky": "^4.3.0", + "prettier": "^2.1.2", + "prettier-plugin-solidity": "^1.0.0-alpha.57", + "pretty-quick": "^3.0.2" + }, + "scripts": { + "lint": "pretty-quick --pattern '**/*.*(sol|json|md)' --verbose", + "lint:check": "prettier --check **/*.sol **/*.json **/*.md", + "lint:fix": "pretty-quick --pattern '**/*.*(sol|json|md)' --staged --verbose" + }, + "husky": { + "hooks": { + "pre-commit": "yarn lint:fix", + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" + } + }, + "dependencies": { + "@poanet/solidity-flattener": "^3.0.6" + } +} diff --git a/protocol/requirements-dev.txt b/protocol/requirements-dev.txt new file mode 100644 index 0000000..4dda48a --- /dev/null +++ b/protocol/requirements-dev.txt @@ -0,0 +1,2 @@ +black==19.10b0 +eth-brownie>=1.11.7,<2.0.0 diff --git a/protocol/tests/aave/test_aave.py b/protocol/tests/aave/test_aave.py new file mode 100644 index 0000000..4dc0734 --- /dev/null +++ b/protocol/tests/aave/test_aave.py @@ -0,0 +1,505 @@ +import pytest +from brownie import * +import typer +import json +import brownie + +# signatures obtained by signing message "Use NFT to access Strategy" using ethersjs +# it is too complicated in python :P + +# choose hint drastic fold phone twice inquiry loud when will scheme grunt +COMMON_USER = '0x967e09C8272FFbAD26A3c71E011FEe375E2431f2' +commonUser = {'from': COMMON_USER} +commonSig = { + "v": 27, + "r": "0x72b2ace924d2a92769919d31347070d5bc6821f83aec3c1ca8a64fa66c06f493", + "s": "0x5c43301a003e24866a46d4435dc328135ddf192fb51f036146e2b639ec25254c" +} + +# remove delay stool artwork mansion settle layer voice leg shock engage liquid +RARE_USER = '0xD9BFc0fD6797f7f3ce42F12EBC6C11c5d19B757C' +rareUser = {'from': RARE_USER} +rareSig = { + "v": 27, + "r": "0xf9978019731cc6f69d4b704c24c1d05f445ef06b74053159278785d76b61a568", + "s": "0x281b7dc0db807712ab53088411ea323319216efc1a333b90cf71cfe21c1c1fd3" +} + +# core comic stage monkey wedding cat rug galaxy grunt hire auto hint +UNIQUE_USER = '0xA30b354F6ed616C20A36E9CB6a5F3324f2fE9349' +uniqueUser = {'from': UNIQUE_USER} +uniqueSig = { + "v": 28, + "r": "0x283d3f7f3a84feef87420303ed7f55e715afaf3edc87f43b07b11a0f53d04492", + "s": "0x35c0382e1fe5e959a0555a6f83bf1c07f9fb202e8b4def540453c0c657fdcdfe" +} + +# without NFT +PLEB_USER = '0x283BeabDc7a42FFC4795026B4F902a34E08ef348' +plebUser = {'from': PLEB_USER} + +# prod, match with constants on contract +commonNFT = 1 +rareNFT = 212 +uniqueNFT = 222 +createLimit = 223 + +# test, match with constants on contract +# commonNFT = 1 +# rareNFT = 5 +# uniqueNFT = 6 +# createLimit = 7 + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts[0] + + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + TREASURY_VAULT = deployed[ENV]['TREASURY_VAULT'] + treasuryVault = Contract(TREASURY_VAULT) + + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + multisig = {'from': GNOSIS_SAFE_PROXY} + + controller = Controller.at(deployed[ENV]['CONTROLLER']) + + USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' + usdc = FiatTokenV2_1.at(USDC) + + STKAAVE = '0x4da27a545c0c5B758a6BA100e3a049001de870f5' + stkAAVE = Contract(STKAAVE) + + WHALE = '0x55fe002aeff02f77364de339a1292923a15844b8' + whale = {'from': WHALE} + + usdc.transfer(COMMON_USER, usdc.balanceOf(WHALE)/4, whale) + usdc.transfer(RARE_USER, usdc.balanceOf(WHALE)/3, whale) + usdc.transfer(UNIQUE_USER, usdc.balanceOf(WHALE)/2, whale) + usdc.transfer(PLEB_USER, usdc.balanceOf(WHALE), whale) + + proxyRegistry = deployed[ENV]['OPENSEA_PROXY_REGISTRY'] + nft = StakeDaoNFT.deploy(proxyRegistry, deployer) + for i in range(1, createLimit): + nft.create(1, 1, "", "", deployer) + nft.safeTransferFrom(DEFAULT_DEPLOYER_ACCOUNT, COMMON_USER, commonNFT, 1, "", deployer); + nft.safeTransferFrom(DEFAULT_DEPLOYER_ACCOUNT, RARE_USER, rareNFT, 1, "", deployer); + nft.safeTransferFrom(DEFAULT_DEPLOYER_ACCOUNT, UNIQUE_USER, uniqueNFT, 1, "", deployer); + + usdcVault = StakeDaoAaveUSDCVault.deploy( + USDC, controller.address, GNOSIS_SAFE_PROXY, nft.address, deployer) + + usdcStrategy = StrategyAaveUSDCLeverage.deploy( + GNOSIS_SAFE_PROXY, controller.address, deployer) + + usdcStrategy.maxApprove(multisig) + + controller.approveStrategy(USDC, usdcStrategy.address, multisig) + controller.setStrategy(USDC, usdcStrategy.address, multisig) + controller.setVault(USDC, usdcVault.address, multisig) + + return {"treasuryVault": treasuryVault, + "multisig": multisig, + "deployer": deployer, + "DEFAULT_DEPLOYER_ACCOUNT": DEFAULT_DEPLOYER_ACCOUNT, + "USDC": USDC, + "usdc": usdc, + "usdcVault": usdcVault, + "usdcStrategy": usdcStrategy, + "controller": controller, + "nft": nft, + "STKAAVE": STKAAVE, + "stkAAVE": stkAAVE, + "TREASURY_VAULT": TREASURY_VAULT} + +def test_usdc_vault_deposit_common(deployedContracts): + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + nft = deployedContracts['nft'] + + shareBefore = usdcVault.balanceOf(COMMON_USER) + senderUSDCBefore = usdc.balanceOf(COMMON_USER) + vaultUSDCBefore = usdc.balanceOf(usdcVault.address) + print('senderUSDCBefore', senderUSDCBefore / 10**6) + + usdc.approve(usdcVault.address, usdc.balanceOf(COMMON_USER), commonUser) + nft.setApprovalForAll(usdcVault.address, True, commonUser) + usdcVault.deposit(9_000 * 10**6, commonNFT, commonSig['v'], commonSig['r'], commonSig['s'], commonUser) + + shareAfter = usdcVault.balanceOf(COMMON_USER) + senderUSDCAfter = usdc.balanceOf(COMMON_USER) + vaultUSDCAfter = usdc.balanceOf(usdcVault.address) + + print("Minted shares", (shareAfter - shareBefore) / 10**6) + + assert shareAfter - shareBefore > 0 + assert senderUSDCBefore - senderUSDCAfter > 0 + assert senderUSDCBefore - senderUSDCAfter == vaultUSDCAfter - vaultUSDCBefore + + +def test_usdc_vault_deposit_rare(deployedContracts): + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + nft = deployedContracts['nft'] + + shareBefore = usdcVault.balanceOf(RARE_USER) + senderUSDCBefore = usdc.balanceOf(RARE_USER) + vaultUSDCBefore = usdc.balanceOf(usdcVault.address) + print('senderUSDCBefore', senderUSDCBefore / 10**6) + + usdc.approve(usdcVault.address, usdc.balanceOf(RARE_USER), rareUser) + nft.setApprovalForAll(usdcVault.address, True, rareUser) + usdcVault.deposit(29_000 * 10**6, rareNFT, rareSig['v'], rareSig['r'], rareSig['s'], rareUser) + + shareAfter = usdcVault.balanceOf(RARE_USER) + senderUSDCAfter = usdc.balanceOf(RARE_USER) + vaultUSDCAfter = usdc.balanceOf(usdcVault.address) + + print("Minted shares", (shareAfter - shareBefore) / 10**6) + + assert shareAfter - shareBefore > 0 + assert senderUSDCBefore - senderUSDCAfter > 0 + assert senderUSDCBefore - senderUSDCAfter == vaultUSDCAfter - vaultUSDCBefore + + +def test_usdc_vault_deposit_unique(deployedContracts): + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + nft = deployedContracts['nft'] + + shareBefore = usdcVault.balanceOf(UNIQUE_USER) + senderUSDCBefore = usdc.balanceOf(UNIQUE_USER) + vaultUSDCBefore = usdc.balanceOf(usdcVault.address) + print('senderUSDCBefore', senderUSDCBefore / 10**6) + + usdc.approve(usdcVault.address, usdc.balanceOf(UNIQUE_USER), uniqueUser) + nft.setApprovalForAll(usdcVault.address, True, uniqueUser) + usdcVault.deposit(119_000 * 10**6, uniqueNFT, uniqueSig['v'], uniqueSig['r'], uniqueSig['s'], uniqueUser) + + shareAfter = usdcVault.balanceOf(UNIQUE_USER) + senderUSDCAfter = usdc.balanceOf(UNIQUE_USER) + vaultUSDCAfter = usdc.balanceOf(usdcVault.address) + + print("Minted shares", (shareAfter - shareBefore) / 10**6) + + assert shareAfter - shareBefore > 0 + assert senderUSDCBefore - senderUSDCAfter > 0 + assert senderUSDCBefore - senderUSDCAfter == vaultUSDCAfter - vaultUSDCBefore + + +def test_deposit_limits_before_release(deployedContracts): + usdcVault = deployedContracts['usdcVault'] + + with brownie.reverts('Exceeds limit'): + usdcVault.deposit(2_000 * 10**6, commonUser) + + with brownie.reverts('Exceeds limit'): + usdcVault.deposit(2_000 * 10**6, rareUser) + + with brownie.reverts('Exceeds limit'): + usdcVault.deposit(2_000 * 10**6, uniqueUser) + +def test_claim_nft_before_release(deployedContracts): + usdcVault = deployedContracts['usdcVault'] + + with brownie.reverts('!publicRelease'): + usdcVault.claimNFT(commonUser) + + with brownie.reverts('!publicRelease'): + usdcVault.claimNFT(rareUser) + + with brownie.reverts('!publicRelease'): + usdcVault.claimNFT(uniqueUser) + +def test_usdc_vault_deposit_pleb(deployedContracts): + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + + usdc.approve(usdcVault.address, usdc.balanceOf(PLEB_USER), plebUser) + with brownie.reverts('NFT not locked'): + usdcVault.deposit(2_000 * 10**6, plebUser) + + chain.sleep(31 * 86400) + usdcVault.deposit(2_000 * 10**6, plebUser) + +def test_deposit_limits_after_release(deployedContracts): + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + nft = deployedContracts['nft'] + + usdcVault.deposit(2_000 * 10**6, commonUser) + usdcVault.deposit(2_000 * 10**6, rareUser) + usdcVault.deposit(2_000 * 10**6, uniqueUser) + +def test_claim_nft_after_release(deployedContracts): + usdcVault = deployedContracts['usdcVault'] + usdcVault.claimNFT(commonUser) + usdcVault.claimNFT(rareUser) + usdcVault.claimNFT(uniqueUser) + +def test_usdc_vault_earn(deployedContracts): + USDC = deployedContracts['USDC'] + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + usdcStrategy = deployedContracts['usdcStrategy'] + deployer = deployedContracts['deployer'] + + vaultUSDCBefore = usdc.balanceOf(usdcVault.address) + + usdcVault.earn(deployer) + + vaultUSDCAfter = usdc.balanceOf(usdcVault.address) + + print("Vault USDC decrease", (vaultUSDCBefore - vaultUSDCAfter) / 10**6) + + position = usdcStrategy.getCurrentPosition() + print("dep, borr", position[0] / 10**6, position[1] / 10**6) + + assert vaultUSDCBefore - vaultUSDCAfter > 0 + assert 0.74 <= position[1] / position[0] <= 0.76 + + +def test_strategy_claim_stkAAVE(deployedContracts): + USDC = deployedContracts['USDC'] + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + usdcStrategy = deployedContracts['usdcStrategy'] + deployer = deployedContracts['deployer'] + multisig = deployedContracts['multisig'] + stkAAVE = deployedContracts['stkAAVE'] + + chain.sleep(5 * 86400) + chain.mine(100) + + stratStkAAVEBefore = stkAAVE.balanceOf(usdcStrategy.address) + usdcStrategy.claimStkAAVE(multisig) + stratStkAAVEAfter = stkAAVE.balanceOf(usdcStrategy.address) + + print("stkAAVE increase", (stratStkAAVEAfter - stratStkAAVEBefore) / 10**18) + + assert stratStkAAVEAfter - stratStkAAVEBefore > 0 + + +def test_strategy_harvest(deployedContracts): + USDC = deployedContracts['USDC'] + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + usdcStrategy = deployedContracts['usdcStrategy'] + deployer = deployedContracts['deployer'] + multisig = deployedContracts['multisig'] + stkAAVE = deployedContracts['stkAAVE'] + TREASURY_VAULT = deployedContracts['TREASURY_VAULT'] + + chain.sleep(11 * 86400) + chain.mine(100) + + position_1 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_1[0] / 10**6, position_1[1] / 10**6) + treasuryUSDCBefore = usdc.balanceOf(TREASURY_VAULT) + print('treasuryUSDCBefore', treasuryUSDCBefore / 10**6) + + usdcStrategy.harvest(multisig) + + treasuryUSDCAfter = usdc.balanceOf(TREASURY_VAULT) + print('treasuryUSDCAfter', treasuryUSDCAfter / 10**6) + position_2 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_2[0] / 10**6, position_2[1] / 10**6) + + assert treasuryUSDCAfter - treasuryUSDCBefore > 0 + assert position_2[0] > position_1[0] + assert position_2[1] > position_1[1] + assert 0.74 <= position_2[1] / position_2[0] <= 0.76 + print("c-ratio", position_2[1] / position_2[0]) + + +def test_vault_withdraw(deployedContracts): + USDC = deployedContracts['USDC'] + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + usdcStrategy = deployedContracts['usdcStrategy'] + deployer = deployedContracts['deployer'] + multisig = deployedContracts['multisig'] + stkAAVE = deployedContracts['stkAAVE'] + TREASURY_VAULT = deployedContracts['TREASURY_VAULT'] + + position_1 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_1[0] / 10**6, position_1[1] / 10**6) + treasuryUSDCBefore = usdc.balanceOf(TREASURY_VAULT) + print('treasuryUSDCBefore', treasuryUSDCBefore / 10**6) + userUSDCBefore = usdc.balanceOf(UNIQUE_USER) + print('userUSDCBefore', userUSDCBefore / 10**6) + + print("strat USDC", usdc.balanceOf(usdcStrategy.address)) + + usdcVault.withdraw(15_000 * 10**6, uniqueUser) + + userUSDCAfter = usdc.balanceOf(UNIQUE_USER) + print('userUSDCAfter', userUSDCAfter / 10**6) + treasuryUSDCAfter = usdc.balanceOf(TREASURY_VAULT) + print('treasuryUSDCAfter', treasuryUSDCAfter / 10**6) + position_2 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_2[0] / 10**6, position_2[1] / 10**6) + + assert userUSDCAfter > userUSDCBefore + assert treasuryUSDCAfter - treasuryUSDCBefore > 0 + assert position_2[0] < position_1[0] + assert position_2[1] < position_1[1] + assert 0.74 <= position_2[1] / position_2[0] <= 0.76 + print("c-ratio", position_2[1] / position_2[0]) + + +def test_vault_withdraw_2(deployedContracts): + USDC = deployedContracts['USDC'] + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + usdcStrategy = deployedContracts['usdcStrategy'] + deployer = deployedContracts['deployer'] + multisig = deployedContracts['multisig'] + stkAAVE = deployedContracts['stkAAVE'] + TREASURY_VAULT = deployedContracts['TREASURY_VAULT'] + + position_1 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_1[0] / 10**6, position_1[1] / 10**6) + treasuryUSDCBefore = usdc.balanceOf(TREASURY_VAULT) + print('treasuryUSDCBefore', treasuryUSDCBefore / 10**6) + userUSDCBefore = usdc.balanceOf(UNIQUE_USER) + print('userUSDCBefore', userUSDCBefore / 10**6) + + usdcVault.withdraw(30_000 * 10**6, uniqueUser) + + userUSDCAfter = usdc.balanceOf(UNIQUE_USER) + print('userUSDCAfter', userUSDCAfter / 10**6) + treasuryUSDCAfter = usdc.balanceOf(TREASURY_VAULT) + print('treasuryUSDCAfter', treasuryUSDCAfter / 10**6) + position_2 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_2[0] / 10**6, position_2[1] / 10**6) + + assert userUSDCAfter > userUSDCBefore + assert treasuryUSDCAfter - treasuryUSDCBefore > 0 + assert position_2[0] < position_1[0] + assert position_2[1] < position_1[1] + print("c-ratio", position_2[1] / position_2[0]) + assert 0.74 <= position_2[1] / position_2[0] <= 0.76 + # chain.snapshot() + + +def test_vault_withdraw_3(deployedContracts): + USDC = deployedContracts['USDC'] + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + usdcStrategy = deployedContracts['usdcStrategy'] + deployer = deployedContracts['deployer'] + multisig = deployedContracts['multisig'] + stkAAVE = deployedContracts['stkAAVE'] + TREASURY_VAULT = deployedContracts['TREASURY_VAULT'] + + position_1 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_1[0] / 10**6, position_1[1] / 10**6) + treasuryUSDCBefore = usdc.balanceOf(TREASURY_VAULT) + print('treasuryUSDCBefore', treasuryUSDCBefore / 10**6) + userUSDCBefore = usdc.balanceOf(UNIQUE_USER) + print('userUSDCBefore', userUSDCBefore / 10**6) + + usdcVault.withdraw(54_000 * 10**6, uniqueUser) + + userUSDCAfter = usdc.balanceOf(UNIQUE_USER) + print('userUSDCAfter', userUSDCAfter / 10**6) + treasuryUSDCAfter = usdc.balanceOf(TREASURY_VAULT) + print('treasuryUSDCAfter', treasuryUSDCAfter / 10**6) + position_2 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_2[0] / 10**6, position_2[1] / 10**6) + + assert userUSDCAfter > userUSDCBefore + assert treasuryUSDCAfter - treasuryUSDCBefore > 0 + assert position_2[0] < position_1[0] + assert position_2[1] < position_1[1] + print("c-ratio", position_2[1] / position_2[0]) + assert 0.74 <= position_2[1] / position_2[0] <= 0.82 + chain.snapshot() + + +def test_vault_withdraw_4(deployedContracts): + USDC = deployedContracts['USDC'] + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + usdcStrategy = deployedContracts['usdcStrategy'] + deployer = deployedContracts['deployer'] + multisig = deployedContracts['multisig'] + stkAAVE = deployedContracts['stkAAVE'] + TREASURY_VAULT = deployedContracts['TREASURY_VAULT'] + + position_1 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_1[0] / 10**6, position_1[1] / 10**6) + treasuryUSDCBefore = usdc.balanceOf(TREASURY_VAULT) + print('treasuryUSDCBefore', treasuryUSDCBefore / 10**6) + userUSDCBefore = usdc.balanceOf(UNIQUE_USER) + print('userUSDCBefore', userUSDCBefore / 10**6) + + print("balance(), _shares, totalSupply()", usdcVault.balance(), + usdcVault.balanceOf(UNIQUE_USER), usdcVault.totalSupply()) + print("balanceOfPool()", usdcStrategy.balanceOfPool()) + + usdcVault.withdraw(usdcVault.balanceOf(UNIQUE_USER), uniqueUser) + + userUSDCAfter = usdc.balanceOf(UNIQUE_USER) + print('userUSDCAfter', userUSDCAfter / 10**6) + treasuryUSDCAfter = usdc.balanceOf(TREASURY_VAULT) + print('treasuryUSDCAfter', treasuryUSDCAfter / 10**6) + position_2 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_2[0] / 10**6, position_2[1] / 10**6) + + assert userUSDCAfter > userUSDCBefore + assert treasuryUSDCAfter - treasuryUSDCBefore > 0 + assert position_2[0] < position_1[0] + assert position_2[1] < position_1[1] + assert position_2[1] == position_2[0] == 0 + + +def test_controller_emergency_withdrawAll_to_vault(deployedContracts): + chain.revert() + USDC = deployedContracts['USDC'] + usdc = deployedContracts['usdc'] + usdcVault = deployedContracts['usdcVault'] + usdcStrategy = deployedContracts['usdcStrategy'] + deployer = deployedContracts['deployer'] + multisig = deployedContracts['multisig'] + stkAAVE = deployedContracts['stkAAVE'] + TREASURY_VAULT = deployedContracts['TREASURY_VAULT'] + controller = deployedContracts['controller'] + + position_1 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_1[0] / 10**6, position_1[1] / 10**6) + vaultUSDCBefore = usdc.balanceOf(usdcVault.address) + print('vaultUSDCBefore', vaultUSDCBefore / 10**6) + print("balanceOfPool() before", usdcStrategy.balanceOfPool()) + + # usdcStrategy.withdrawToVault(usdcStrategy.balanceOfPool(), multisig) + controller.withdrawAll(USDC, multisig) + + print("balanceOfPool() after", usdcStrategy.balanceOfPool()) + vaultUSDCAfter = usdc.balanceOf(usdcVault.address) + print('vaultUSDCAfter', vaultUSDCAfter / 10**6) + position_2 = usdcStrategy.getCurrentPosition() + print("dep, borr", position_2[0] / 10**6, position_2[1] / 10**6) + + assert vaultUSDCAfter > vaultUSDCBefore + assert vaultUSDCAfter - vaultUSDCBefore >= usdcStrategy.balanceOfPool() + assert position_2[0] < position_1[0] + assert position_2[1] < position_1[1] + assert position_2[1] == position_2[0] == 0 diff --git a/protocol/tests/backscratcher/conftest.py b/protocol/tests/backscratcher/conftest.py new file mode 100644 index 0000000..df285f4 --- /dev/null +++ b/protocol/tests/backscratcher/conftest.py @@ -0,0 +1,66 @@ +import pytest +from itertools import chain + + +# @pytest.fixture(scope="function", autouse=True) +# def shared_setup(fn_isolation): +# pass + + +@pytest.fixture +def user(accounts): + return accounts.at("0x431e81e5dfb5a24541b5ff8762bdef3f32f96354", force=True) + + +@pytest.fixture +def crv(interface): + return interface.ERC20("0xD533a949740bb3306d119CC777fa900bA034cd52") + + +@pytest.fixture +def lp_3crv(interface): + return interface.ERC20("0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490") + + +@pytest.fixture +def y3crv(interface): + return interface.yVault("0x9cA85572E6A3EbF24dEDd195623F188735A5179f") + + +@pytest.fixture +def vault(interface): + return interface.veCurveVault("0xc5bDdf9843308380375a611c18B50Fb9341f502A") + + +@pytest.fixture +def vesting(interface): + return interface.CurveVesting("0x575CCD8e2D300e2377B43478339E364000318E2c") + + +@pytest.fixture +def minter(interface): + return interface.CurveMinter("0xd061D61a4d941c39E5453435B6345Dc261C2fcE0") + + +@pytest.fixture +def gauges(interface, user): + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + registry = interface.CurveRegistry( + "0x7D86446dDb609eD0F5f8684AcF30380a356b2B4c") + pools = [registry.pool_list(i) for i in range(registry.pool_count())] + gauges = set(chain.from_iterable( + [registry.get_gauges(pool)[0] for pool in pools])) + gauges.discard(ZERO_ADDRESS) + gauges = [interface.CurveGauge(g) for g in gauges] + gauges += [ZERO_ADDRESS for _ in range(20 - len(gauges))] + return gauges[:20] + + +@pytest.fixture +def backzapper(accounts, CurveBackzapper): + return CurveBackzapper.deploy({"from": accounts[0]}) + + +@pytest.fixture +def zap_3crv(accounts, y3CrvZapper): + return y3CrvZapper.deploy({"from": accounts[0]}) diff --git a/protocol/tests/backscratcher/test_backscratcher.py b/protocol/tests/backscratcher/test_backscratcher.py new file mode 100644 index 0000000..68e31c6 --- /dev/null +++ b/protocol/tests/backscratcher/test_backscratcher.py @@ -0,0 +1,445 @@ +import pytest +from brownie import * +import typer +import json + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # list of 10 use-n-throw addresses + addresses = ['0x4ce799e6eD8D64536b67dD428565d52A531B3640', '0x4e14672D1Ed1FEFAc9948456006a4015D186C15E', '0x6EDfA9fb275f389a641c8C3c85DdF29dF2Ca8EC7', '0x57813f12d90177bE50ec25Fb60bd2BaC1558fd9C', '0xF65fA4C233CDA7aA7a3563d824809231b138cf11', + '0x10Cdd8CDaa3c402AA2fc3d6a29b6D79424f77fDB', '0x8E60704Fa16bE888a0EcBFD68DF34DfC9D066A15', '0x6FE46dd39f8289218f99448DEdAFa2fD4C483aDc', '0x0D935F1Da128b34698Dc3274a9F39bAF2D6C79eE', '0x02a22054d42C50797dcBFeE3077C767b2c9864EB'] + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts.at(addresses[0], force=True) + + # Existing GnosisSafe mainnet address + GNOSISSAFE_MASTERCOPY = '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F' + + # Existing GnosisSafeProxyFactory mainnet address + GNOSISSAFE_PROXY_FACTORY = '0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B' + + # Curve TokenMinter mainnet address + TOKEN_MINTER = '0xd061D61a4d941c39E5453435B6345Dc261C2fcE0' + + # YEARN_DEPLOYER = accounts.at(addresses[1], force=True) + + # an array of 3 owners of governance multisig + GOVERNANCE_OWNERS = [] + for i in range(2, 5): + GOVERNANCE_OWNERS.append(accounts.at(addresses[i], force=True)) + + # Strategist account + STRATEGIST = accounts.at(addresses[5], force=True) + + # trx data for setting up owners and threshold for governance multisig + DATA = "0xb63e800d0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000004e14672d1ed1fefac9948456006a4015d186c15e0000000000000000000000006edfa9fb275f389a641c8c3c85ddf29df2ca8ec700000000000000000000000057813f12d90177be50ec25fb60bd2bac1558fd9c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000" + + # DAI mainnet token address + DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' + + # sbtcCrv Token mainnet address + SBTC_CRV = '0x075b1bb99792c9E1041bA13afEf80C91a1e70fB3' + + # 3Crv Token mainnet address + THREE_CRV = '0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490' + + # eursCrv Token mainnet address + EURS_CRV = '0x194eBd173F6cDacE046C53eACcE9B953F28411d1' + + # Keepr address + Keep3r = '0x1cEB5cB57C4D4E2b2433641b95Dd330A33185A44' + + STRATEGY_PROXY = deployed[ENV]['STRATEGY_PROXY'] + + # StrategyProxy mainnet address + # StrategyProxyAddress = '0x7A1848e7847F3f5FfB4d8e63BdB9569db535A4f0' + + # 3Crv minter + THREE_CRV_MINTER = '0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7' + curve3Pool = Contract(THREE_CRV_MINTER) + + # sbtc minter + SBTC_CRV_MINTER = '0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714' + + # eursCrv minter + EURS_CRV_MINTER = '0x0Ce6a5fF5217e38315f87032CF90686C96627CAA' + + # DAI bags + DAI_HOLDER = '0x70178102AA04C5f0E54315aA958601eC9B7a4E08' + + # Curve Voting Escrow + VOTING_ESCROW = '0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2' + votingEscrow = Contract(VOTING_ESCROW) + + FEE_DISTRI = '0xA464e6DCda8AC41e03616F95f4BC98a13b8922Dc' + feeDistri = Contract(FEE_DISTRI) + + # number of SDT minted per block at the time of deployment + SDT_PER_BLOCK = 1000 + + sender = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + CRV_HOLDER = '0x4ce799e6eD8D64536b67dD428565d52A531B3640' + + crv = CRV.at('0xD533a949740bb3306d119CC777fa900bA034cd52') + + crvBal = crv.balanceOf(CRV_HOLDER) + + crv.transfer(DEFAULT_DEPLOYER_ACCOUNT, crvBal, {'from': CRV_HOLDER}) + + dai = VaultToken.at(DAI) + + daiBal = dai.balanceOf(DAI_HOLDER) + + dai.transfer(DEFAULT_DEPLOYER_ACCOUNT, daiBal, {'from': DAI_HOLDER}) + + # CurveYCRVVoter mainnet address + CURVE_YCRV_VOTER = deployed[ENV]['CURVE_YCRV_VOTER'] + # CURVE_YCRV_VOTER = '0xF147b8125d2ef93FB6965Db97D6746952a133934' + curveYCRVVoter = CurveYCRVVoter.at(CURVE_YCRV_VOTER) + + VE_CURVE_VAULT = deployed[ENV]['VE_CURVE_VAULT'] + # VE_CURVE_VAULT = '0xc5bDdf9843308380375a611c18B50Fb9341f502A' + veCurve = veCurveVault.at(VE_CURVE_VAULT) + + sbtc = VaultToken.at(SBTC_CRV) + threeCrv = VaultToken.at(THREE_CRV) + eursCrv = VaultToken.at(EURS_CRV) + + sbtcBal = 10000000000000000000000000000 + threeCrvBal = 10000000000000000000000000000 + eursCrvBal = 10000000000000000000000000000 + + # threeCrv.mint(DEFAULT_DEPLOYER_ACCOUNT, threeCrvBal, + # {'from': THREE_CRV_MINTER}) + # eursCrv.mint(DEFAULT_DEPLOYER_ACCOUNT, eursCrvBal, + # {'from': EURS_CRV_MINTER}) + # sbtc.mint(DEFAULT_DEPLOYER_ACCOUNT, sbtcBal, {'from': SBTC_CRV_MINTER}) + + # threeCrv.approve(threePoolVault.address, threeCrvBal, sender) + # eursCrv.approve(eursCrvVault.address, eursCrvBal, sender) + # sbtc.approve(sbtcVault.address, sbtcBal, sender) + # MALICE = '0x55FE002aefF02F77364de339a1292923A15844B8' + # PLEB = '0x0E33Be39B13c576ff48E14392fBf96b02F40Cd34' + # PLEB = '0xD6216fC19DB775Df9774a6E33526131dA7D19a2c' + PLEB = '0x6512cbdad4d76ff79d3e96ace168f2e1315c1ece' + pleb = {'from': PLEB} + + # return {'threePoolVault': threePoolVault, 'eursCrvVault': eursCrvVault, 'sbtcVault': sbtcVault, 'sender': sender, 'DEFAULT_DEPLOYER_ACCOUNT': DEFAULT_DEPLOYER_ACCOUNT, 'eursCrv': eursCrv, 'threeCrv': threeCrv, 'sbtc': sbtc, 'strategyCurveEursCrvVoterProxy': strategyCurveEursCrvVoterProxy, 'strategyCurve3CrvVoterProxy': strategyCurve3CrvVoterProxy, 'strategyCurveBTCVoterProxy': strategyCurveBTCVoterProxy, 'veCurve': veCurve, 'crv': crv, 'crvBal': crvBal, 'threeCrvBal': threeCrvBal, 'eursCrvBal': eursCrvBal, 'sbtcBal': sbtcBal, 'veCurve': veCurve, 'STRATEGIST': STRATEGIST, 'controller': controller, 'governanceGSPAddress': governanceGSPAddress, 'daiBal': daiBal, 'dai': dai, 'sdtToken': sdtToken, 'masterchef': masterchef, 'timelockGovernance': timelockGovernance, 'treasuryVault': treasuryVault, 'governanceStaking': governanceStaking} + return {'sender': sender, 'DEFAULT_DEPLOYER_ACCOUNT': DEFAULT_DEPLOYER_ACCOUNT, + 'crv': crv, 'crvBal': crvBal, 'veCurve': veCurve, 'votingEscrow': votingEscrow, + 'curveYCRVVoter': curveYCRVVoter, 'curve3Pool': curve3Pool, 'threeCrv': threeCrv, + 'feeDistri': feeDistri, 'pleb': pleb, 'STRATEGY_PROXY': STRATEGY_PROXY} + + +################################## veCurveVault tests ##################################### +# pleb - small investor, malice - bad-actor +# In plain english: +# 1. Team deposits 1 weiCRV +# 2. Team claims 305 3CRV +# 3. Pleb deposits +# 4. Malice claims on empty vault after 100 days +# 5. Pleb claims on filled vault +# 6. Large depositor claims on empty vault after 50 days (with snapshot, revert to state after (2)) +# 7. Malice claims on filled vault after 90 days (with snapshot, revert to state after (2)) + + +def test_veCurveVault_team_deposit(deployedContracts): + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + crv = deployedContracts['crv'] + crvBal = deployedContracts['crvBal'] + veCurve = deployedContracts['veCurve'] + votingEscrow = deployedContracts['votingEscrow'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + + DEPOSIT_AMOUNT = 10**18 + crv.approve(veCurve.address, 10 * (10**6) * (10**18), sender) + + weightBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + lockedBefore = votingEscrow.locked(curveYCRVVoter.address) + # sender is assumed as team's address + veCurve.deposit(DEPOSIT_AMOUNT, sender) + + weightAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + lockedAfter = votingEscrow.locked(curveYCRVVoter.address) + yveCrvBal = veCurve.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + # lockedAfter[0] = amount, lockedAfter[1] = end + assert yveCrvBal == DEPOSIT_AMOUNT + assert weightAfter > weightBefore + assert lockedAfter[0] == lockedBefore[0] + DEPOSIT_AMOUNT + + +def test_veCurveVault_team_claim(deployedContracts): + sender = deployedContracts['sender'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + + deployer3CrvBefore = threeCrv.balanceOf(sender['from']) + yCrvVoter3CrvBefore = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvBefore = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvBefore team-claim', veCurve3CrvBefore) + + veCurve.claim(sender) + + deployer3CrvAfter = threeCrv.balanceOf(sender['from']) + yCrvVoter3CrvAfter = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvAfter = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvAfter = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvAfter team-claim', veCurve3CrvAfter) + + assert deployer3CrvAfter > deployer3CrvBefore + assert yCrvVoter3CrvAfter == yCrvVoter3CrvBefore + assert stratProxy3CrvAfter == stratProxy3CrvBefore + chain.snapshot() + + +def test_increase_unlock_time(deployedContracts): + sender = deployedContracts['sender'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + crv = deployedContracts['crv'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + votingEscrow = deployedContracts['votingEscrow'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + + # To 24/02/2022 + chain.sleep(365 * 1 * 84600) + # 1738800000 = 06/02/2025 + lockedBefore = votingEscrow.locked(curveYCRVVoter.address) + print("lockedBefore", lockedBefore[1]) + # 1758964777 = 24/9/2025 + data = votingEscrow.increase_unlock_time.encode_input(1766740777) + curveYCRVVoter.execute(votingEscrow.address, 0, data, { + 'from': '0xf930ebbd05ef8b25b1797b9b2109ddc9b0d43063'}) + lockedAfter = votingEscrow.locked(curveYCRVVoter.address) + print("lockedAfter", lockedAfter[1]) + assert lockedAfter[1] > lockedBefore[1] + + +def test_veCurveVault_pleb_deposit(deployedContracts): + chain.revert() + sender = deployedContracts['sender'] + crv = deployedContracts['crv'] + crvBal = deployedContracts['crvBal'] + veCurve = deployedContracts['veCurve'] + votingEscrow = deployedContracts['votingEscrow'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + + DEPOSIT_AMOUNT = 100 * (10**18) + crv.approve(veCurve.address, 10**9 * (10**18), pleb) + + weightBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + lockedBefore = votingEscrow.locked(curveYCRVVoter.address) + + veCurve.deposit(DEPOSIT_AMOUNT, pleb) + + weightAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + lockedAfter = votingEscrow.locked(curveYCRVVoter.address) + yveCrvBal = veCurve.balanceOf(pleb['from']) + # lockedAfter[0] = amount, lockedAfter[1] = end + assert yveCrvBal == DEPOSIT_AMOUNT + assert weightAfter > weightBefore + assert lockedAfter[0] == lockedBefore[0] + DEPOSIT_AMOUNT + + +def test_malice_claim_on_empty_vault(deployedContracts): + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + MALICE = '0x10Cdd8CDaa3c402AA2fc3d6a29b6D79424f77fDB' + + malice3CrvBefore = threeCrv.balanceOf(MALICE) + vault3CrvBefore = threeCrv.balanceOf(veCurve.address) + yCrvVoter3CrvBefore = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvBefore = threeCrv.balanceOf(STRATEGY_PROXY) + print('veCurve3CrvBefore malice-claim', vault3CrvBefore) + # 100 days in future to accumulate admin fees + chain.sleep(100*24*60*60) + # Malice trying to claim without depositing any CRV + veCurve.claim({'from': MALICE}) + + malice3CrvAfter = threeCrv.balanceOf(MALICE) + vault3CrvAfter = threeCrv.balanceOf(veCurve.address) + yCrvVoter3CrvAfter = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvAfter = threeCrv.balanceOf(STRATEGY_PROXY) + print('veCurve3CrvAfter malice-claim', vault3CrvAfter) + + assert malice3CrvAfter == malice3CrvBefore + assert vault3CrvAfter > vault3CrvBefore + assert yCrvVoter3CrvAfter == yCrvVoter3CrvBefore + assert stratProxy3CrvAfter == stratProxy3CrvBefore + + +def test_pleb_depositor_claim_on_filled_vault(deployedContracts): + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + + pleb3CrvBalBefore = threeCrv.balanceOf(pleb['from']) + yCrvVoter3CrvBefore = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvBefore = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvBefore pleb-claim', veCurve3CrvBefore) + # chain.sleep(100*24*60*60) + veCurve.claim(pleb) + veCurve.claim(pleb) + + pleb3CrvBalAfter = threeCrv.balanceOf(pleb['from']) + yCrvVoter3CrvAfter = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvAfter = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvAfter = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvAfter pleb-claim', veCurve3CrvAfter) + + assert pleb3CrvBalAfter > pleb3CrvBalBefore + assert yCrvVoter3CrvAfter == yCrvVoter3CrvBefore + assert stratProxy3CrvAfter == stratProxy3CrvBefore + + +def test_large_depositor_claim_on_almost_empty_vault(deployedContracts): + chain.revert() + sender = deployedContracts['sender'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + crv = deployedContracts['crv'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + votingEscrow = deployedContracts['votingEscrow'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + CRV_WHALE = '0x0E33Be39B13c576ff48E14392fBf96b02F40Cd34' + crv_whale = {'from': CRV_WHALE} + + crv.approve(veCurve.address, 10**9 * (10**18), crv_whale) + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + + # assert veCurve3CrvBefore < 10**18 + # veCurve.deposit(100 * 10**18, crv_whale) + veCurve.depositAll(crv_whale) + chain.sleep(50*24*60*60) + + crvWhale3CrvBalBefore = threeCrv.balanceOf(CRV_WHALE) + yCrvVoter3CrvBefore = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvBefore = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvBefore', veCurve3CrvBefore) + + veCurve.claim(crv_whale) + + crvWhale3CrvBalAfter = threeCrv.balanceOf(CRV_WHALE) + yCrvVoter3CrvAfter = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvAfter = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvAfter = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvAfter', veCurve3CrvAfter) + + assert crvWhale3CrvBalAfter > crvWhale3CrvBalBefore + assert yCrvVoter3CrvAfter == yCrvVoter3CrvBefore + assert stratProxy3CrvAfter == stratProxy3CrvBefore + + +def test_malice_claim_on_filled_vault(deployedContracts): + chain.revert() + sender = deployedContracts['sender'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + crv = deployedContracts['crv'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + votingEscrow = deployedContracts['votingEscrow'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + MALICE_1 = '0x55FE002aefF02F77364de339a1292923A15844B8' + MALICE_2 = '0xD6216fC19DB775Df9774a6E33526131dA7D19a2c' + CRV_WHALE = '0xf89501b77b2fa6329f94f5a05fe84cebb5c8b1a0' + crv_whale = {'from': CRV_WHALE} + + crv.approve(veCurve.address, 10**9 * (10**18), crv_whale) + # veCurve.deposit(100 * 10**18, crv_whale) + veCurve.depositAll(crv_whale) + chain.sleep(90*24*60*60) + + crvWhale3CrvBalBefore = threeCrv.balanceOf(CRV_WHALE) + yCrvVoter3CrvBefore = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvBefore = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + print("veCurve3CrvBefore malice1-claim 1", veCurve3CrvBefore) + + tx = veCurve.claim({'from': MALICE_1}) + # tx.call_trace() + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + print("veCurve3CrvBefore malice1-claim 2", veCurve3CrvBefore) + # vault is now filled, though sometimes fees collected might be 0.1 3CRV + assert veCurve3CrvBefore > 10**18 + malice2_3CrvBalBefore = threeCrv.balanceOf(MALICE_2) + + veCurve.claim({'from': MALICE_2}) + + malice2_3CrvBalAfter = threeCrv.balanceOf(MALICE_2) + yCrvVoter3CrvAfter = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvAfter = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvAfter = threeCrv.balanceOf(veCurve.address) + print("veCurve3CrvBefore malice1-claim 3", veCurve3CrvAfter) + + assert malice2_3CrvBalAfter == malice2_3CrvBalBefore + assert veCurve3CrvAfter == veCurve3CrvBefore + assert yCrvVoter3CrvAfter == yCrvVoter3CrvBefore + assert stratProxy3CrvAfter == stratProxy3CrvBefore + + +def log(msg): + msg = typer.style(msg, fg=typer.colors.GREEN, bold=True) + typer.echo(msg) + + # DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' + # USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + # USDC_WHALE = '0x55FE002aefF02F77364de339a1292923A15844B8' + # dai = VaultToken.at(DAI) + # usdc = VaultToken.at(USDC) + # usdc.approve(curve3Pool.address, 10**9 * 10**6, {'from': USDC_WHALE}) + # dai.approve(curve3Pool.address, 10**9 * 10**18, {'from': USDC_WHALE}) + # # DAI = 0, USDC = 1, USDT = 2 + # curve3Pool.exchange(1, 0, 100 * 10**6 * 10**6, 10 * + # 10**18, {'from': USDC_WHALE}) + # print("DAI_BAL", dai.balanceOf(USDC_WHALE)) + + # curve3Pool.exchange(0, 1, 99 * 10**6 * 10**18, 10 * + # 10**6, {'from': USDC_WHALE}) + # print("USDC_BAL", usdc.balanceOf(USDC_WHALE)) diff --git a/protocol/tests/backscratcher/test_redeployment.py b/protocol/tests/backscratcher/test_redeployment.py new file mode 100644 index 0000000..814988b --- /dev/null +++ b/protocol/tests/backscratcher/test_redeployment.py @@ -0,0 +1,882 @@ +import pytest +from brownie import * +import typer +import json + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # list of 10 use-n-throw addresses + addresses = ['0x4ce799e6eD8D64536b67dD428565d52A531B3640', '0x4e14672D1Ed1FEFAc9948456006a4015D186C15E', '0x6EDfA9fb275f389a641c8C3c85DdF29dF2Ca8EC7', '0x57813f12d90177bE50ec25Fb60bd2BaC1558fd9C', '0xF65fA4C233CDA7aA7a3563d824809231b138cf11', + '0x10Cdd8CDaa3c402AA2fc3d6a29b6D79424f77fDB', '0x8E60704Fa16bE888a0EcBFD68DF34DfC9D066A15', '0x6FE46dd39f8289218f99448DEdAFa2fD4C483aDc', '0x0D935F1Da128b34698Dc3274a9F39bAF2D6C79eE', '0x02a22054d42C50797dcBFeE3077C767b2c9864EB'] + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts.at(addresses[0], force=True) + # DEFAULT_DEPLOYER_ACCOUNT = accounts.load('stakedao-deployer-rug-pull') + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + # Existing GnosisSafe mainnet address + GNOSISSAFE_MASTERCOPY = '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F' + + # Existing GnosisSafeProxyFactory mainnet address + GNOSISSAFE_PROXY_FACTORY = '0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B' + + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + multisig = {'from': GNOSIS_SAFE_PROXY} + + # Curve TokenMinter mainnet address + TOKEN_MINTER = '0xd061D61a4d941c39E5453435B6345Dc261C2fcE0' + + # YEARN_DEPLOYER = accounts.at(addresses[1], force=True) + + # an array of 3 owners of governance multisig + GOVERNANCE_OWNERS = [] + for i in range(2, 5): + GOVERNANCE_OWNERS.append(accounts.at(addresses[i], force=True)) + + # Strategist account + STRATEGIST = accounts.at(addresses[5], force=True) + + # trx data for setting up owners and threshold for governance multisig + DATA = "0xb63e800d0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000004e14672d1ed1fefac9948456006a4015d186c15e0000000000000000000000006edfa9fb275f389a641c8c3c85ddf29df2ca8ec700000000000000000000000057813f12d90177be50ec25fb60bd2bac1558fd9c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000" + + # DAI mainnet token address + DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' + + # sbtcCrv Token mainnet address + SBTC_CRV = '0x075b1bb99792c9E1041bA13afEf80C91a1e70fB3' + + # 3Crv Token mainnet address + THREE_CRV = '0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490' + + # eursCrv Token mainnet address + EURS_CRV = '0x194eBd173F6cDacE046C53eACcE9B953F28411d1' + + # Keepr address + Keep3r = '0x1cEB5cB57C4D4E2b2433641b95Dd330A33185A44' + + STRATEGY_PROXY = deployed[ENV]['STRATEGY_PROXY'] + # Deploy StrategyProxy + strategyProxy = StrategyProxy.at(STRATEGY_PROXY) + + strategyCurveEursCrvVoterProxy = StrategyCurveEursCrvVoterProxy.at( + deployed[ENV]['STRATEGY_CURVE_EURS_CRV_VOTER_PROXY']) + + strategyCurveBTCVoterProxy = StrategyCurveBTCVoterProxy.at( + deployed[ENV]['STRATEGY_CURVE_BTC_VOTER_PROXY']) + + strategyCurve3CrvVoterProxy = StrategyCurve3CrvVoterProxy.at( + deployed[ENV]['STRATEGY_CURVE_3CRV_VOTER_PROXY']) + + # 3Crv minter + THREE_CRV_MINTER = '0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7' + curve3Pool = Contract(THREE_CRV_MINTER) + + # sbtc minter + SBTC_CRV_MINTER = '0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714' + + # eursCrv minter + EURS_CRV_MINTER = '0x0Ce6a5fF5217e38315f87032CF90686C96627CAA' + + # DAI bags + DAI_HOLDER = '0x70178102AA04C5f0E54315aA958601eC9B7a4E08' + + # Curve Voting Escrow + VOTING_ESCROW = '0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2' + votingEscrow = Contract(VOTING_ESCROW) + + FEE_DISTRI = '0xA464e6DCda8AC41e03616F95f4BC98a13b8922Dc' + feeDistri = Contract(FEE_DISTRI) + + # number of SDT minted per block at the time of deployment + SDT_PER_BLOCK = 1000 + + sender = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + CRV_HOLDER = '0x4ce799e6eD8D64536b67dD428565d52A531B3640' + + crv = CRV.at('0xD533a949740bb3306d119CC777fa900bA034cd52') + + crvBal = crv.balanceOf(CRV_HOLDER) + + crv.transfer(DEFAULT_DEPLOYER_ACCOUNT, crvBal, {'from': CRV_HOLDER}) + + dai = VaultToken.at(DAI) + + daiBal = dai.balanceOf(DAI_HOLDER) + + dai.transfer(DEFAULT_DEPLOYER_ACCOUNT, daiBal, {'from': DAI_HOLDER}) + + # CurveYCRVVoter mainnet address + CURVE_YCRV_VOTER = deployed[ENV]['CURVE_YCRV_VOTER'] + # CURVE_YCRV_VOTER = '0xF147b8125d2ef93FB6965Db97D6746952a133934' + curveYCRVVoter = CurveYCRVVoter.at(CURVE_YCRV_VOTER) + + VE_CURVE_VAULT = deployed[ENV]['VE_CURVE_VAULT'] + # VE_CURVE_VAULT = '0xc5bDdf9843308380375a611c18B50Fb9341f502A' + veCurve = veCurveVault.at(VE_CURVE_VAULT) + + sbtcCrv = VaultToken.at(SBTC_CRV) + threeCrv = VaultToken.at(THREE_CRV) + eursCrv = VaultToken.at(EURS_CRV) + + EURS_CRV_VAULT = deployed[ENV]['EURS_CRV_VAULT'] + # THREE_POOL_VAULT + THREE_POOL_VAULT = "0xB17640796e4c27a39AF51887aff3F8DC0daF9567" + # SBTC_VAULT + SBTC_VAULT = "0x24129B935AfF071c4f0554882C0D9573F4975fEd" + + eursCrvVault = yVault.at(EURS_CRV_VAULT) + threePoolVault = yVault.at(THREE_POOL_VAULT) + sbtcCrvVault = yVault.at(SBTC_VAULT) + + sbtcBal = 10000000000000000000000000000 + threeCrvBal = 10000000000000000000000000000 + eursCrvBal = 10000000000000000000000000000 + + CONTROLLER = deployed[ENV]['CONTROLLER'] + controller = Controller.at(CONTROLLER) + + TREASURY_VAULT = deployed[ENV]['TREASURY_VAULT'] + treasuryVault = TreasuryVault.at(TREASURY_VAULT) + + EURS_CRV = deployed[ENV]['EURS_CRV'] + eursCrv = VaultToken.at(EURS_CRV) + + SNX = "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F" + EURS = "0xdB25f211AB05b1c97D595516F45794528a807ad8" + snx = Contract(SNX) + eurs = Contract(EURS) + + # threeCrv.mint(DEFAULT_DEPLOYER_ACCOUNT, threeCrvBal, + # {'from': THREE_CRV_MINTER}) + # eursCrv.mint(DEFAULT_DEPLOYER_ACCOUNT, eursCrvBal, + # {'from': EURS_CRV_MINTER}) + # sbtc.mint(DEFAULT_DEPLOYER_ACCOUNT, sbtcBal, {'from': SBTC_CRV_MINTER}) + + # threeCrv.approve(threePoolVault.address, threeCrvBal, sender) + # eursCrv.approve(eursCrvVault.address, eursCrvBal, sender) + # sbtc.approve(sbtcVault.address, sbtcBal, sender) + # MALICE = '0x55FE002aefF02F77364de339a1292923A15844B8' + # PLEB = '0x0E33Be39B13c576ff48E14392fBf96b02F40Cd34' + # PLEB = '0xD6216fC19DB775Df9774a6E33526131dA7D19a2c' + PLEB = '0x6512cbdad4d76ff79d3e96ace168f2e1315c1ece' + pleb = {'from': PLEB} + + # return {'threePoolVault': threePoolVault, 'eursCrvVault': eursCrvVault, 'sbtcVault': sbtcVault, 'sender': sender, 'DEFAULT_DEPLOYER_ACCOUNT': DEFAULT_DEPLOYER_ACCOUNT, 'eursCrv': eursCrv, 'threeCrv': threeCrv, 'sbtc': sbtc, 'strategyCurveEursCrvVoterProxy': strategyCurveEursCrvVoterProxy, 'strategyCurve3CrvVoterProxy': strategyCurve3CrvVoterProxy, 'strategyCurveBTCVoterProxy': strategyCurveBTCVoterProxy, 'veCurve': veCurve, 'crv': crv, 'crvBal': crvBal, 'threeCrvBal': threeCrvBal, 'eursCrvBal': eursCrvBal, 'sbtcBal': sbtcBal, 'veCurve': veCurve, 'STRATEGIST': STRATEGIST, 'controller': controller, 'governanceGSPAddress': governanceGSPAddress, 'daiBal': daiBal, 'dai': dai, 'sdtToken': sdtToken, 'masterchef': masterchef, 'timelockGovernance': timelockGovernance, 'treasuryVault': treasuryVault, 'governanceStaking': governanceStaking} + return {'sender': sender, 'DEFAULT_DEPLOYER_ACCOUNT': DEFAULT_DEPLOYER_ACCOUNT, + 'crv': crv, 'crvBal': crvBal, 'veCurve': veCurve, 'votingEscrow': votingEscrow, + 'curveYCRVVoter': curveYCRVVoter, 'curve3Pool': curve3Pool, 'threeCrv': threeCrv, + 'feeDistri': feeDistri, 'pleb': pleb, 'STRATEGY_PROXY': STRATEGY_PROXY, + 'multisig': multisig, 'strategyProxy': strategyProxy, 'strategyCurveEursCrvVoterProxy': strategyCurveEursCrvVoterProxy, + 'strategyCurveBTCVoterProxy': strategyCurveBTCVoterProxy, 'strategyCurve3CrvVoterProxy': strategyCurve3CrvVoterProxy, + 'GNOSIS_SAFE_PROXY': GNOSIS_SAFE_PROXY, 'deployer': deployer, 'eursCrvVault': eursCrvVault, + 'threePoolVault': threePoolVault, 'sbtcCrvVault': sbtcCrvVault, 'controller': controller, + 'treasuryVault': treasuryVault, 'EURS_CRV': EURS_CRV, 'sbtcCrv': sbtcCrv, 'eursCrv': eursCrv, + 'snx': snx, 'eurs': eurs} + + +################################## veCurveVault tests ##################################### +# pleb - small investor, malice - bad-actor +# In plain english: +# 1. Team deposits 1 weiCRV +# 2. Team claims 305 3CRV +# 3. Pleb deposits +# 4. Malice claims on empty vault after 100 days +# 5. Pleb claims on filled vault +# 6. Large depositor claims on empty vault after 50 days (with snapshot, revert to state after (2)) +# 7. Malice claims on filled vault after 90 days (with snapshot, revert to state after (2)) + + +def test_veCurveVault_team_deposit(deployedContracts): + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + crv = deployedContracts['crv'] + crvBal = deployedContracts['crvBal'] + veCurve = deployedContracts['veCurve'] + votingEscrow = deployedContracts['votingEscrow'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + + DEPOSIT_AMOUNT = 10**18 + crv.approve(veCurve.address, 10 * (10**6) * (10**18), sender) + + weightBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + lockedBefore = votingEscrow.locked(curveYCRVVoter.address) + # sender is assumed as team's address + veCurve.deposit(DEPOSIT_AMOUNT, sender) + + weightAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + lockedAfter = votingEscrow.locked(curveYCRVVoter.address) + yveCrvBal = veCurve.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + # lockedAfter[0] = amount, lockedAfter[1] = end + assert yveCrvBal == DEPOSIT_AMOUNT + assert weightAfter > weightBefore + assert lockedAfter[0] == lockedBefore[0] + DEPOSIT_AMOUNT + + +def test_veCurveVault_team_claim(deployedContracts): + sender = deployedContracts['sender'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + + deployer3CrvBefore = threeCrv.balanceOf(sender['from']) + yCrvVoter3CrvBefore = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvBefore = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvBefore team-claim', veCurve3CrvBefore) + + veCurve.claim(sender) + + deployer3CrvAfter = threeCrv.balanceOf(sender['from']) + yCrvVoter3CrvAfter = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvAfter = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvAfter = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvAfter team-claim', veCurve3CrvAfter) + + assert deployer3CrvAfter > deployer3CrvBefore + assert yCrvVoter3CrvAfter == yCrvVoter3CrvBefore + assert stratProxy3CrvAfter == stratProxy3CrvBefore + chain.snapshot() + + +def test_increase_unlock_time(deployedContracts): + sender = deployedContracts['sender'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + crv = deployedContracts['crv'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + votingEscrow = deployedContracts['votingEscrow'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + + # To 24/02/2022 + chain.sleep(365 * 1 * 84600) + # 1738800000 = 06/02/2025 + lockedBefore = votingEscrow.locked(curveYCRVVoter.address) + print("lockedBefore", lockedBefore[1]) + # 1758964777 = 24/9/2025 + data = votingEscrow.increase_unlock_time.encode_input(1766740777) + curveYCRVVoter.execute(votingEscrow.address, 0, data, { + 'from': '0xf930ebbd05ef8b25b1797b9b2109ddc9b0d43063'}) + lockedAfter = votingEscrow.locked(curveYCRVVoter.address) + print("lockedAfter", lockedAfter[1]) + assert lockedAfter[1] > lockedBefore[1] + + +def test_veCurveVault_pleb_deposit(deployedContracts): + chain.revert() + sender = deployedContracts['sender'] + crv = deployedContracts['crv'] + crvBal = deployedContracts['crvBal'] + veCurve = deployedContracts['veCurve'] + votingEscrow = deployedContracts['votingEscrow'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + + DEPOSIT_AMOUNT = 100 * (10**18) + crv.approve(veCurve.address, 10**9 * (10**18), pleb) + + weightBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + lockedBefore = votingEscrow.locked(curveYCRVVoter.address) + + veCurve.deposit(DEPOSIT_AMOUNT, pleb) + + weightAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + lockedAfter = votingEscrow.locked(curveYCRVVoter.address) + yveCrvBal = veCurve.balanceOf(pleb['from']) + # lockedAfter[0] = amount, lockedAfter[1] = end + assert yveCrvBal == DEPOSIT_AMOUNT + assert weightAfter > weightBefore + assert lockedAfter[0] == lockedBefore[0] + DEPOSIT_AMOUNT + + +def test_malice_claim_on_empty_vault(deployedContracts): + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + MALICE = '0x10Cdd8CDaa3c402AA2fc3d6a29b6D79424f77fDB' + + malice3CrvBefore = threeCrv.balanceOf(MALICE) + vault3CrvBefore = threeCrv.balanceOf(veCurve.address) + yCrvVoter3CrvBefore = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvBefore = threeCrv.balanceOf(STRATEGY_PROXY) + print('veCurve3CrvBefore malice-claim', vault3CrvBefore) + # 100 days in future to accumulate admin fees + chain.sleep(100*24*60*60) + # Malice trying to claim without depositing any CRV + veCurve.claim({'from': MALICE}) + + malice3CrvAfter = threeCrv.balanceOf(MALICE) + vault3CrvAfter = threeCrv.balanceOf(veCurve.address) + yCrvVoter3CrvAfter = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvAfter = threeCrv.balanceOf(STRATEGY_PROXY) + print('veCurve3CrvAfter malice-claim', vault3CrvAfter) + + assert malice3CrvAfter == malice3CrvBefore + assert vault3CrvAfter > vault3CrvBefore + assert yCrvVoter3CrvAfter == yCrvVoter3CrvBefore + assert stratProxy3CrvAfter == stratProxy3CrvBefore + + +def test_pleb_depositor_claim_on_filled_vault(deployedContracts): + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + + pleb3CrvBalBefore = threeCrv.balanceOf(pleb['from']) + yCrvVoter3CrvBefore = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvBefore = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvBefore pleb-claim', veCurve3CrvBefore) + # chain.sleep(100*24*60*60) + veCurve.claim(pleb) + veCurve.claim(pleb) + + pleb3CrvBalAfter = threeCrv.balanceOf(pleb['from']) + yCrvVoter3CrvAfter = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvAfter = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvAfter = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvAfter pleb-claim', veCurve3CrvAfter) + + assert pleb3CrvBalAfter > pleb3CrvBalBefore + assert yCrvVoter3CrvAfter == yCrvVoter3CrvBefore + assert stratProxy3CrvAfter == stratProxy3CrvBefore + + +def test_large_depositor_claim_on_almost_empty_vault(deployedContracts): + chain.revert() + sender = deployedContracts['sender'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + crv = deployedContracts['crv'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + votingEscrow = deployedContracts['votingEscrow'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + CRV_WHALE = '0x0E33Be39B13c576ff48E14392fBf96b02F40Cd34' + crv_whale = {'from': CRV_WHALE} + + crv.approve(veCurve.address, 10**9 * (10**18), crv_whale) + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + + # assert veCurve3CrvBefore < 10**18 + # veCurve.deposit(100 * 10**18, crv_whale) + veCurve.depositAll(crv_whale) + chain.sleep(50*24*60*60) + + crvWhale3CrvBalBefore = threeCrv.balanceOf(CRV_WHALE) + yCrvVoter3CrvBefore = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvBefore = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvBefore', veCurve3CrvBefore) + + veCurve.claim(crv_whale) + + crvWhale3CrvBalAfter = threeCrv.balanceOf(CRV_WHALE) + yCrvVoter3CrvAfter = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvAfter = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvAfter = threeCrv.balanceOf(veCurve.address) + print('veCurve3CrvAfter', veCurve3CrvAfter) + + assert crvWhale3CrvBalAfter > crvWhale3CrvBalBefore + assert yCrvVoter3CrvAfter == yCrvVoter3CrvBefore + assert stratProxy3CrvAfter == stratProxy3CrvBefore + + +def test_malice_claim_on_filled_vault(deployedContracts): + chain.revert() + sender = deployedContracts['sender'] + veCurve = deployedContracts['veCurve'] + curve3Pool = deployedContracts['curve3Pool'] + crv = deployedContracts['crv'] + threeCrv = deployedContracts['threeCrv'] + feeDistri = deployedContracts['feeDistri'] + votingEscrow = deployedContracts['votingEscrow'] + curveYCRVVoter = deployedContracts['curveYCRVVoter'] + pleb = deployedContracts['pleb'] + STRATEGY_PROXY = deployedContracts['STRATEGY_PROXY'] + MALICE_1 = '0x55FE002aefF02F77364de339a1292923A15844B8' + MALICE_2 = '0xD6216fC19DB775Df9774a6E33526131dA7D19a2c' + CRV_WHALE = '0xf89501b77b2fa6329f94f5a05fe84cebb5c8b1a0' + crv_whale = {'from': CRV_WHALE} + + crv.approve(veCurve.address, 10**9 * (10**18), crv_whale) + # veCurve.deposit(100 * 10**18, crv_whale) + veCurve.depositAll(crv_whale) + chain.sleep(90*24*60*60) + + crvWhale3CrvBalBefore = threeCrv.balanceOf(CRV_WHALE) + yCrvVoter3CrvBefore = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvBefore = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + print("veCurve3CrvBefore malice1-claim 1", veCurve3CrvBefore) + + tx = veCurve.claim({'from': MALICE_1}) + # tx.call_trace() + veCurve3CrvBefore = threeCrv.balanceOf(veCurve.address) + print("veCurve3CrvBefore malice1-claim 2", veCurve3CrvBefore) + # vault is now filled, though sometimes fees collected might be 0.1 3CRV + assert veCurve3CrvBefore > 10**18 + malice2_3CrvBalBefore = threeCrv.balanceOf(MALICE_2) + + veCurve.claim({'from': MALICE_2}) + + malice2_3CrvBalAfter = threeCrv.balanceOf(MALICE_2) + yCrvVoter3CrvAfter = threeCrv.balanceOf(curveYCRVVoter.address) + stratProxy3CrvAfter = threeCrv.balanceOf(STRATEGY_PROXY) + veCurve3CrvAfter = threeCrv.balanceOf(veCurve.address) + print("veCurve3CrvBefore malice1-claim 3", veCurve3CrvAfter) + + assert malice2_3CrvBalAfter == malice2_3CrvBalBefore + assert veCurve3CrvAfter == veCurve3CrvBefore + assert yCrvVoter3CrvAfter == yCrvVoter3CrvBefore + assert stratProxy3CrvAfter == stratProxy3CrvBefore + +################ StrategyProxy tests ######################### + + +def test_strategyCurveEursCrvVoterProxy_approveStrategy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # Register strategyCurveEursCrvVoterProxy in StrategyProxy + # strategyProxy.approveStrategy( + # strategyCurveEursCrvVoterProxy.address, deployer) + + assert strategyProxy.strategies( + strategyCurveEursCrvVoterProxy.address) == True + + +def test_strategyCurveEursCrvVoterProxy_setProxy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # set proxy to StrategyProxy + # strategyCurveEursCrvVoterProxy.setProxy( + # strategyProxy.address, deployer) + + assert strategyCurveEursCrvVoterProxy.proxy() == strategyProxy.address + + +def test_strategyCurveBTCVoterProxy_approveStrategy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurveBTCVoterProxy = deployedContracts["strategyCurveBTCVoterProxy"] + deployer = deployedContracts["deployer"] + + # Register strategyCurveBTCVoterProxy in StrategyProxy + # strategyProxy.approveStrategy(strategyCurveBTCVoterProxy.address, deployer) + + assert strategyProxy.strategies( + strategyCurveBTCVoterProxy.address) == True + + +def test_strategyCurve3CrvVoterProxy_approveStrategy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurve3CrvVoterProxy = deployedContracts["strategyCurve3CrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # Register strategyCurve3CrvVoterProxy in StrategyProxy + # strategyProxy.approveStrategy( + # strategyCurve3CrvVoterProxy.address, deployer) + + assert strategyProxy.strategies( + strategyCurve3CrvVoterProxy.address) == True + + +def test_strategyProxy_setGovernance(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + GNOSIS_SAFE_PROXY = deployedContracts["GNOSIS_SAFE_PROXY"] + deployer = deployedContracts["deployer"] + + # Transfer StrategyProxy governance to Governance (GSP) + # strategyProxy.setGovernance(GNOSIS_SAFE_PROXY, deployer) + + assert strategyProxy.governance() == GNOSIS_SAFE_PROXY + + +def test_veCurve_acceptGovernance(deployedContracts): + veCurve = deployedContracts["veCurve"] + GNOSIS_SAFE_PROXY = deployedContracts["GNOSIS_SAFE_PROXY"] + multisig = deployedContracts["multisig"] + + # multisig accept Governance of veCurve vault + # veCurve.acceptGovernance(multisig) + + assert veCurve.governance() == GNOSIS_SAFE_PROXY + + +def test_veCurve_setFeeDistribution(deployedContracts): + veCurve = deployedContracts["veCurve"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # Set Curve FeeDistribution contract to new strategyProxy + # veCurve.setFeeDistribution(strategyProxy.address, multisig) + + assert veCurve.feeDistribution() == strategyProxy.address + + +def test_veCurve_setProxy(deployedContracts): + veCurve = deployedContracts["veCurve"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # Set new StrategyProxy + # veCurve.setProxy(strategyProxy.address, multisig) + + assert veCurve.proxy() == strategyProxy.address + + +def test_strategyCurve3CrvVoterProxy_setProxy(deployedContracts): + strategyCurve3CrvVoterProxy = deployedContracts["strategyCurve3CrvVoterProxy"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # set proxy to new StrategyProxy + # strategyCurve3CrvVoterProxy.setProxy(strategyProxy.address, multisig) + + assert strategyCurve3CrvVoterProxy.proxy() == strategyProxy.address + + +def test_strategyCurveBTCVoterProxy_setProxy(deployedContracts): + strategyCurveBTCVoterProxy = deployedContracts["strategyCurveBTCVoterProxy"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # set proxy to new StrategyProxy + # strategyCurveBTCVoterProxy.setProxy(strategyProxy.address, multisig) + + assert strategyCurveBTCVoterProxy.proxy() == strategyProxy.address + + +def test_curveYCRVVoter_setStrategy(deployedContracts): + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # Set CurveYCRVVoter strategy to new StrategyProxy + # curveYCRVVoter.setStrategy(strategyProxy.address, multisig) + + assert curveYCRVVoter.strategy() == strategyProxy.address + + +def test_eursCrvVault_earn(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + controller = deployedContracts["controller"] + + totalBalBefore = eursCrvVault.balance() + vaultBalBefore = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + + # assert stratBalBefore == 0 + # assert totalBalBefore == vaultBalBefore + + # Call earn on eursCrv Vault to transfer the funds to the new strategy + eursCrvVault.earn(deployer) + + totalBalAfter = eursCrvVault.balance() + vaultBalAfter = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + + assert totalBalBefore == totalBalAfter + assert stratBalAfter > 0 + assert vaultBalBefore > vaultBalAfter + assert vaultBalBefore < vaultBalAfter + stratBalAfter + + assert crvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursCrvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert eursCrvBal(deployedContracts, strategyProxy) == 0 + assert eursBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_strategyCurveEursCrvVoterProxy_harvest(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + multisig = deployedContracts["multisig"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + votingEscrow = deployedContracts["votingEscrow"] + controller = deployedContracts["controller"] + treasuryVault = deployedContracts["treasuryVault"] + + chain.mine(1000) + + totalBalBefore = eursCrvVault.balance() + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + earnedBefore = strategyCurveEursCrvVoterProxy.earned() + lockedBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalBefore = eursCrvBal( + deployedContracts, treasuryVault.address) + + # assert earnedBefore == 0 + + strategyCurveEursCrvVoterProxy.harvest(multisig) + + totalBalAfter = eursCrvVault.balance() + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + earnedAfter = strategyCurveEursCrvVoterProxy.earned() + lockedAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalAfter = eursCrvBal( + deployedContracts, treasuryVault.address) + + log("Total eursCrv Earned in 1000 blocks: " + + str((earnedAfter-earnedBefore)/10**18)) + log("Change in total eursCrv in Vault+Strategy+Gauge: " + + str((totalBalAfter-totalBalBefore)/10**18)) + log("Change in total eursCrv in Strategy+Gauge: " + + str((stratBalAfter-stratBalBefore)/10**18)) + + assert earnedAfter - earnedBefore > 0 + assert totalBalAfter > totalBalBefore + assert stratBalAfter > stratBalBefore + assert lockedAfter > lockedBefore + assert treasuryVaultBalAfter > treasuryVaultBalBefore + + assert crvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursCrvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert eursCrvBal(deployedContracts, strategyProxy) == 0 + assert eursBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_strategyCurve3CrvVoterProxy_harvest(deployedContracts): + strategyCurve3CrvVoterProxy = deployedContracts["strategyCurve3CrvVoterProxy"] + deployer = deployedContracts["deployer"] + threePoolVault = deployedContracts["threePoolVault"] + multisig = deployedContracts["multisig"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + votingEscrow = deployedContracts["votingEscrow"] + controller = deployedContracts["controller"] + treasuryVault = deployedContracts["treasuryVault"] + + chain.mine(1000) + + totalBalBefore = threePoolVault.balance() + stratBalBefore = strategyCurve3CrvVoterProxy.balanceOf() + earnedBefore = strategyCurve3CrvVoterProxy.earned() + lockedBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalBefore = threeCrvBal( + deployedContracts, treasuryVault.address) + + strategyCurve3CrvVoterProxy.harvest(multisig) + + totalBalAfter = threePoolVault.balance() + stratBalAfter = strategyCurve3CrvVoterProxy.balanceOf() + earnedAfter = strategyCurve3CrvVoterProxy.earned() + lockedAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalAfter = threeCrvBal( + deployedContracts, treasuryVault.address) + + log("Total 3Crv Earned in 1000 blocks: " + + str((earnedAfter-earnedBefore)/10**18)) + log("Change in total 3Crv in Vault+Strategy+Gauge: " + + str((totalBalAfter-totalBalBefore)/10**18)) + log("Change in total 3Crv in Strategy+Gauge: " + + str((stratBalAfter-stratBalBefore)/10**18)) + + assert earnedAfter - earnedBefore > 0 + assert totalBalAfter > totalBalBefore + assert stratBalAfter > stratBalBefore + assert treasuryVaultBalAfter > treasuryVaultBalBefore + + assert crvBal(deployedContracts, strategyCurve3CrvVoterProxy) == 0 + assert threeCrvBal(deployedContracts, strategyCurve3CrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurve3CrvVoterProxy) == 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert threeCrvBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert threeCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + assert lockedAfter > lockedBefore + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_strategyCurveBTCVoterProxy_harvest(deployedContracts): + strategyCurveBTCVoterProxy = deployedContracts["strategyCurveBTCVoterProxy"] + deployer = deployedContracts["deployer"] + sbtcCrvVault = deployedContracts["sbtcCrvVault"] + multisig = deployedContracts["multisig"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + votingEscrow = deployedContracts["votingEscrow"] + controller = deployedContracts["controller"] + treasuryVault = deployedContracts["treasuryVault"] + + chain.mine(1000) + + totalBalBefore = sbtcCrvVault.balance() + stratBalBefore = strategyCurveBTCVoterProxy.balanceOf() + earnedBefore = strategyCurveBTCVoterProxy.earned() + lockedBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalBefore = sbtcCrvBal( + deployedContracts, treasuryVault.address) + + strategyCurveBTCVoterProxy.harvest(multisig) + + totalBalAfter = sbtcCrvVault.balance() + stratBalAfter = strategyCurveBTCVoterProxy.balanceOf() + earnedAfter = strategyCurveBTCVoterProxy.earned() + lockedAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalAfter = sbtcCrvBal( + deployedContracts, treasuryVault.address) + + log("Total sbtcCrv Earned in 1000 blocks: " + + str((earnedAfter-earnedBefore)/10**18)) + log("Change in total sbtcCrv in Vault+Strategy+Gauge: " + + str((totalBalAfter-totalBalBefore)/10**18)) + log("Change in total sbtcCrv in Strategy+Gauge: " + + str((stratBalAfter-stratBalBefore)/10**18)) + + assert earnedAfter - earnedBefore > 0 + assert totalBalAfter > totalBalBefore + assert stratBalAfter > stratBalBefore + assert treasuryVaultBalAfter > treasuryVaultBalBefore + + assert crvBal(deployedContracts, strategyCurveBTCVoterProxy) == 0 + assert sbtcCrvBal(deployedContracts, strategyCurveBTCVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveBTCVoterProxy) == 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert sbtcCrvBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert sbtcCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + assert lockedAfter > lockedBefore + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def crvBal(deployedContracts, address): + crv = deployedContracts["crv"] + return crv.balanceOf(address) + + +def eursCrvBal(deployedContracts, address): + eursCrv = deployedContracts["eursCrv"] + return eursCrv.balanceOf(address) + + +def eursBal(deployedContracts, address): + eurs = deployedContracts["eurs"] + return eurs.balanceOf(address) + + +def threeCrvBal(deployedContracts, address): + threeCrv = deployedContracts["threeCrv"] + return threeCrv.balanceOf(address) + + +def sbtcCrvBal(deployedContracts, address): + sbtcCrvBal = deployedContracts["sbtcCrv"] + return sbtcCrvBal.balanceOf(address) + + +def snxBal(deployedContracts, address): + snx = deployedContracts["snx"] + return snx.balanceOf(address) + + +def log(msg): + msg = typer.style(msg, fg=typer.colors.GREEN, bold=True) + typer.echo(msg) + + # DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' + # USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + # USDC_WHALE = '0x55FE002aefF02F77364de339a1292923A15844B8' + # dai = VaultToken.at(DAI) + # usdc = VaultToken.at(USDC) + # usdc.approve(curve3Pool.address, 10**9 * 10**6, {'from': USDC_WHALE}) + # dai.approve(curve3Pool.address, 10**9 * 10**18, {'from': USDC_WHALE}) + # # DAI = 0, USDC = 1, USDT = 2 + # curve3Pool.exchange(1, 0, 100 * 10**6 * 10**6, 10 * + # 10**18, {'from': USDC_WHALE}) + # print("DAI_BAL", dai.balanceOf(USDC_WHALE)) + + # curve3Pool.exchange(0, 1, 99 * 10**6 * 10**18, 10 * + # 10**6, {'from': USDC_WHALE}) + # print("USDC_BAL", usdc.balanceOf(USDC_WHALE)) diff --git a/protocol/tests/backscratcher/test_zap.py b/protocol/tests/backscratcher/test_zap.py new file mode 100644 index 0000000..0b1d680 --- /dev/null +++ b/protocol/tests/backscratcher/test_zap.py @@ -0,0 +1,33 @@ +import pytest + + +def test_claim(user, lp_3crv, vault, accounts): + three_gauge = accounts.at("0xbFcF63294aD7105dEa65aA58F8AE5BE2D9d0952A", force=True) + lp_3crv.transfer(vault, "1000 ether", {"from": three_gauge}) + before = lp_3crv.balanceOf(user) + vault.claim({"from": user}) + assert lp_3crv.balanceOf(user) > before + + +def test_backzapper(backzapper, user, crv, vesting, vault, minter, gauges): + before = vault.balanceOf(user) + assert vesting.balanceOf(user) > 0 + minter.toggle_approve_mint(backzapper, {"from": user}) + crv.approve(backzapper, 2 ** 256 - 1, {"from": user}) + backzapper.zap(gauges, {"from": user}) + assert vault.balanceOf(user) > before + assert crv.balanceOf(user) == 0 + assert vesting.balanceOf(user) == 0 + assert crv.balanceOf(backzapper) == 0 + + +def test_3crv_zapper(zap_3crv, lp_3crv, y3crv, vault, accounts, user): + gauge_3crv = accounts.at("0xbFcF63294aD7105dEa65aA58F8AE5BE2D9d0952A", force=True) + lp_3crv.transfer(vault, "1000 ether", {"from": gauge_3crv}) + before = y3crv.balanceOf(user) + lp_3crv.approve(zap_3crv, 2 ** 256 - 1, {"from": user}) + zap_3crv.zap({"from": user}) + assert y3crv.balanceOf(user) > before + assert lp_3crv.balanceOf(user) == 0 + assert lp_3crv.balanceOf(zap_3crv) == 0 + assert y3crv.balanceOf(zap_3crv) == 0 diff --git a/protocol/tests/core/__init__.py b/protocol/tests/core/__init__.py new file mode 100644 index 0000000..7b0a236 --- /dev/null +++ b/protocol/tests/core/__init__.py @@ -0,0 +1 @@ +# Just here to disambiguate the test files (can't use same name without a module) diff --git a/protocol/tests/core/test_core.py b/protocol/tests/core/test_core.py new file mode 100644 index 0000000..7b96106 --- /dev/null +++ b/protocol/tests/core/test_core.py @@ -0,0 +1,1577 @@ +import pytest +from brownie import * + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + # list of 10 use-n-throw addresses + addresses = ['0x4ce799e6eD8D64536b67dD428565d52A531B3640', '0x4e14672D1Ed1FEFAc9948456006a4015D186C15E', '0x6EDfA9fb275f389a641c8C3c85DdF29dF2Ca8EC7', '0x57813f12d90177bE50ec25Fb60bd2BaC1558fd9C', '0xF65fA4C233CDA7aA7a3563d824809231b138cf11', + '0x10Cdd8CDaa3c402AA2fc3d6a29b6D79424f77fDB', '0x8E60704Fa16bE888a0EcBFD68DF34DfC9D066A15', '0x6FE46dd39f8289218f99448DEdAFa2fD4C483aDc', '0x0D935F1Da128b34698Dc3274a9F39bAF2D6C79eE', '0x02a22054d42C50797dcBFeE3077C767b2c9864EB'] + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts.at(addresses[0], force=True) + + # Existing GnosisSafe mainnet address + GNOSISSAFE_MASTERCOPY = '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F' + + # Existing GnosisSafeProxyFactory mainnet address + GNOSISSAFE_PROXY_FACTORY = '0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B' + + # Curve TokenMinter mainnet address + TOKEN_MINTER = '0xd061D61a4d941c39E5453435B6345Dc261C2fcE0' + + # YEARN_DEPLOYER = accounts.at(addresses[1], force=True) + + # an array of 3 owners of governance multisig + GOVERNANCE_OWNERS = [] + for i in range(2, 5): + GOVERNANCE_OWNERS.append(accounts.at(addresses[i], force=True)) + + # Strategist account + STRATEGIST = accounts.at(addresses[5], force=True) + + # trx data for setting up owners and threshold for governance multisig + DATA = "0xb63e800d0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000004e14672d1ed1fefac9948456006a4015d186c15e0000000000000000000000006edfa9fb275f389a641c8c3c85ddf29df2ca8ec700000000000000000000000057813f12d90177be50ec25fb60bd2bac1558fd9c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000" + + # DAI mainnet token address + DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' + + # sbtcCrv Token mainnet address + SBTC_CRV = '0x075b1bb99792c9E1041bA13afEf80C91a1e70fB3' + + # 3Crv Token mainnet address + THREE_CRV = '0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490' + + # eursCrv Token mainnet address + EURS_CRV = '0x194eBd173F6cDacE046C53eACcE9B953F28411d1' + + # Keepr address + Keep3r = '0x1cEB5cB57C4D4E2b2433641b95Dd330A33185A44' + + # CurveYCRVVoter mainnet address + # CurveYCRVVoter = '0xF147b8125d2ef93FB6965Db97D6746952a133934' + + # StrategyProxy mainnet address + # StrategyProxyAddress = '0x7A1848e7847F3f5FfB4d8e63BdB9569db535A4f0' + + # 3Crv minter + THREE_CRV_MINTER = '0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7' + + # sbtc minter + SBTC_CRV_MINTER = '0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714' + + # eursCrv minter + EURS_CRV_MINTER = '0x0Ce6a5fF5217e38315f87032CF90686C96627CAA' + + # DAI bags + DAI_HOLDER = '0x70178102AA04C5f0E54315aA958601eC9B7a4E08' + + # number of SDT minted per block at the time of deployment + SDT_PER_BLOCK = 1000 + + sender = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + CRV_HOLDER = '0x4ce799e6eD8D64536b67dD428565d52A531B3640' + + crv = CRV.at('0xD533a949740bb3306d119CC777fa900bA034cd52') + + crvBal = crv.balanceOf(CRV_HOLDER) + + crv.transfer(DEFAULT_DEPLOYER_ACCOUNT, crvBal, {'from': CRV_HOLDER}) + + dai = VaultToken.at(DAI) + + daiBal = dai.balanceOf(DAI_HOLDER) + + dai.transfer(DEFAULT_DEPLOYER_ACCOUNT, daiBal, {'from': DAI_HOLDER}) + + # Deploy GnosisSafeProxyFactory + governanceProxyFactory = GnosisSafeProxyFactory.at( + GNOSISSAFE_PROXY_FACTORY) + + # Deploy GnosisSafeProxy (Governance) and setup owners, threashold + governanceGSPTx = governanceProxyFactory.createProxyWithNonce( + GNOSISSAFE_MASTERCOPY, DATA, 0, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + governanceGSPAddress = governanceGSPTx.new_contracts[0] + + # Deploy SDT + sdtToken = SDT.deploy({'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Mint SDT + sdtSupply = 1000000000000000000000 + sdtToken.mint(DEFAULT_DEPLOYER_ACCOUNT, sdtSupply, sender) + + # Deploy TimelockGovernance + timelockGovernance = Timelock.deploy( + governanceGSPAddress, 28800, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy MasterChef + masterchef = MasterChef.deploy( + sdtToken, governanceGSPAddress, SDT_PER_BLOCK, web3.eth.blockNumber, 0, sender) + + # Transferring the MasterChef contract ownership to the TimelockGovernance + # masterchef.transferOwnership(timelockGovernance.address, sender) + + # Transferring the SDT contract ownership to the MasterChef + sdtToken.transferOwnership(masterchef.address, sender) + + # Deploy GovernanceStaking + governanceStaking = StakeDaoGovernance.deploy( + {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Initialize Governance Staking + governanceStaking.initialize(0, governanceGSPAddress, DAI, sdtToken.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy TreasuryZap + treasuryZap = TreasuryZap.deploy({'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy TreasuryVault + treasuryVault = TreasuryVault.deploy(governanceStaking.address, DAI, treasuryZap.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy CurveYCRVVoter + curveYCRVVoter = CurveYCRVVoter.deploy({'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Whitelist CurveYCRVVoter + smartWalletWhitelist = Contract( + '0xca719728Ef172d0961768581fdF35CB116e0B7a4') + smartWalletWhitelist.approveWallet( + curveYCRVVoter.address, {'from': '0x40907540d8a6C65c637785e8f8B742ae6b0b9968'}) + + # Fund CurveYCRVVoter to create a lock + crv.transfer(curveYCRVVoter.address, 1, sender) + + # Update crvBal + crvBal = crv.balanceOf(CRV_HOLDER) + + # Create a lock in Curve Voting Escrow + curveYCRVVoter.createLock( + 1, chain.time()+126144000, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy StrategyProxy + strategyProxy = StrategyProxy.deploy({'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Set CurveYCRVVoter strategy to StrategyProxy + curveYCRVVoter.setStrategy(strategyProxy.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer CurveYCRVVoter governance to Governance (GSP) + curveYCRVVoter.setGovernance(governanceGSPAddress, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy veCurveVault + veCurve = veCurveVault.deploy({'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy Controller + controller = Controller.deploy(treasuryVault.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy CrvStrategyKeep3r + crvStrategyKeep3r = CrvStrategyKeep3r.deploy( + DEFAULT_DEPLOYER_ACCOUNT, sender) + + # setKeep3r + crvStrategyKeep3r.setKeep3r(Keep3r, sender) + + # Deploy eursCrv Vault + eursCrvVault = yVault.deploy(EURS_CRV, controller.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer eursCrv Vault governance to Governance (GSP) + eursCrvVault.setGovernance(governanceGSPAddress, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy strategyCurveEursCrvVoterProxy + strategyCurveEursCrvVoterProxy = StrategyCurveEursCrvVoterProxy.deploy( + controller.address, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Register strategyCurveEursCrvVoterProxy in StrategyProxy + strategyProxy.approveStrategy(strategyCurveEursCrvVoterProxy.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # set proxy to StrategyProxy + strategyCurveEursCrvVoterProxy.setProxy( + strategyProxy.address, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer strategyCurveEursCrvVoterProxy strategist role to CrvStrategyKeep3r + strategyCurveEursCrvVoterProxy.setStrategist( + STRATEGIST, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer strategyCurveEursCrvVoterProxy governance to Governance (GSP) + strategyCurveEursCrvVoterProxy.setGovernance( + governanceGSPAddress, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Approve the strategyCurveEursCrvVoterProxy strategy in Controller + controller.approveStrategy(EURS_CRV, strategyCurveEursCrvVoterProxy.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Register strategyCurveEursCrvVoterProxy in Controller + controller.setStrategy(EURS_CRV, strategyCurveEursCrvVoterProxy.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Register eursCrv Vault in Controller + controller.setVault(EURS_CRV, eursCrvVault.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy 3pool Vault + threePoolVault = yVault.deploy(THREE_CRV, controller.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer 3pool Vault governance to Governance (GSP) + threePoolVault.setGovernance(governanceGSPAddress, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy StrategyCurve3CrvVoterProxy + strategyCurve3CrvVoterProxy = StrategyCurve3CrvVoterProxy.deploy( + controller.address, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Register strategyCurve3CrvVoterProxy in StrategyProxy + strategyProxy.approveStrategy(strategyCurve3CrvVoterProxy.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # set proxy to StrategyProxy + strategyCurve3CrvVoterProxy.setProxy( + strategyProxy.address, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer StrategyCurve3CrvVoterProxy strategist role to CrvStrategyKeep3r + strategyCurve3CrvVoterProxy.setStrategist( + STRATEGIST, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer StrategyCurve3CrvVoterProxy governance to Governance (GSP) + strategyCurve3CrvVoterProxy.setGovernance( + governanceGSPAddress, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Approve the StrategyCurve3CrvVoterProxy strategy in Controller + controller.approveStrategy(THREE_CRV, strategyCurve3CrvVoterProxy.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Register StrategyCurve3CrvVoterProxy in Controller + controller.setStrategy(THREE_CRV, strategyCurve3CrvVoterProxy.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Register 3pool Vault in Controller + controller.setVault(THREE_CRV, threePoolVault.address, + {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy sbtc Vault + sbtcVault = yVault.deploy(SBTC_CRV, controller.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer sbtc Vault governance to Governance (GSP) + sbtcVault.setGovernance(governanceGSPAddress, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Deploy StrategyCurveBTCVoterProxy + strategyCurveBTCVoterProxy = StrategyCurveBTCVoterProxy.deploy( + controller.address, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Register strategyCurveBTCVoterProxy in StrategyProxy + strategyProxy.approveStrategy(strategyCurveBTCVoterProxy.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # set proxy to StrategyProxy + strategyCurveBTCVoterProxy.setProxy( + strategyProxy.address, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer StrategyCurveBTCVoterProxy strategist role to CrvStrategyKeep3r + strategyCurveBTCVoterProxy.setStrategist( + STRATEGIST, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer StrategyCurveBTCVoterProxy governance to Governance (GSP) + strategyCurveBTCVoterProxy.setGovernance( + governanceGSPAddress, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Approve the StrategyCurveBTCVoterProxy strategy in Controller + controller.approveStrategy(SBTC_CRV, strategyCurveBTCVoterProxy.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Register StrategyCurveBTCVoterProxy in Controller + controller.setStrategy(SBTC_CRV, strategyCurveBTCVoterProxy.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Register sbtc Vault in Controller + controller.setVault(SBTC_CRV, sbtcVault.address, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # set Strategist for Controller + controller.setStrategist(STRATEGIST, {'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer Controller governance to Governance (GSP) + # TODO: This transfer of governance should be ususally done just after + # the creation of the Controller. But we delay the governance + # transfer for simplicity. + controller.setGovernance(governanceGSPAddress, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + # Transfer StrategyProxy governance to Governance (GSP) + strategyProxy.setGovernance(governanceGSPAddress, { + 'from': DEFAULT_DEPLOYER_ACCOUNT}) + + bal = threePoolVault.balance() + + sbtc = VaultToken.at(SBTC_CRV) + threeCrv = VaultToken.at(THREE_CRV) + eursCrv = VaultToken.at(EURS_CRV) + + sbtcBal = 10000000000000000000000000000 + threeCrvBal = 10000000000000000000000000000 + eursCrvBal = 10000000000000000000000000000 + + threeCrv.mint(DEFAULT_DEPLOYER_ACCOUNT, threeCrvBal, + {'from': THREE_CRV_MINTER}) + eursCrv.mint(DEFAULT_DEPLOYER_ACCOUNT, eursCrvBal, + {'from': EURS_CRV_MINTER}) + sbtc.mint(DEFAULT_DEPLOYER_ACCOUNT, sbtcBal, {'from': SBTC_CRV_MINTER}) + + threeCrv.approve(threePoolVault.address, threeCrvBal, sender) + eursCrv.approve(eursCrvVault.address, eursCrvBal, sender) + sbtc.approve(sbtcVault.address, sbtcBal, sender) + + return {'threePoolVault': threePoolVault, 'eursCrvVault': eursCrvVault, 'sbtcVault': sbtcVault, 'sender': sender, 'DEFAULT_DEPLOYER_ACCOUNT': DEFAULT_DEPLOYER_ACCOUNT, 'eursCrv': eursCrv, 'threeCrv': threeCrv, 'sbtc': sbtc, 'strategyCurveEursCrvVoterProxy': strategyCurveEursCrvVoterProxy, 'strategyCurve3CrvVoterProxy': strategyCurve3CrvVoterProxy, 'strategyCurveBTCVoterProxy': strategyCurveBTCVoterProxy, 'veCurve': veCurve, 'crv': crv, 'crvBal': crvBal, 'threeCrvBal': threeCrvBal, 'eursCrvBal': eursCrvBal, 'sbtcBal': sbtcBal, 'veCurve': veCurve, 'STRATEGIST': STRATEGIST, 'controller': controller, 'governanceGSPAddress': governanceGSPAddress, 'daiBal': daiBal, 'dai': dai, 'sdtToken': sdtToken, 'masterchef': masterchef, 'timelockGovernance': timelockGovernance, 'treasuryVault': treasuryVault, 'governanceStaking': governanceStaking} + + +""" @pytest.fixture(autouse=True) +def isolation(fn_isolation): + print("isolation fixture") + pass """ + +################################## Treasury tests ##################################### + + +def test_Treasury_swapViaZap(deployedContracts): + treasuryVault = deployedContracts['treasuryVault'] + sbtc = deployedContracts['sbtc'] + dai = deployedContracts['dai'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + treasuryVault.setAuthorized(DEFAULT_DEPLOYER_ACCOUNT, sender) + + sbtc.transfer(treasuryVault.address, 200000000000000000, sender) + + beforeSbtcBal = sbtc.balanceOf(treasuryVault.address) + beforeDaiBal = dai.balanceOf(treasuryVault.address) + + tx = treasuryVault.swapViaZap( + sbtc.address, dai.address, 100000000000000000, sender) + + amountOut = tx.return_value + + afterSbtcBal = sbtc.balanceOf(treasuryVault.address) + afterDaiBal = dai.balanceOf(treasuryVault.address) + + print("Amount Out", amountOut) + + assert afterSbtcBal == beforeSbtcBal - 100000000000000000 + assert afterDaiBal == beforeDaiBal + amountOut + + +def test_Treasury_toGovernance(deployedContracts): + treasuryVault = deployedContracts['treasuryVault'] + sbtc = deployedContracts['sbtc'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + beforeGovBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeTreasuryBal = sbtc.balanceOf(treasuryVault.address) + + treasuryVault.toGovernance(sbtc.address, beforeTreasuryBal, sender) + + afterGovBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterTreasuryBal = sbtc.balanceOf(treasuryVault.address) + + assert afterGovBal == beforeGovBal + beforeTreasuryBal + assert afterTreasuryBal == 0 + + +def test_Treasury_toGovernanceStaking(deployedContracts): + treasuryVault = deployedContracts['treasuryVault'] + dai = deployedContracts['dai'] + sender = deployedContracts['sender'] + governanceStaking = deployedContracts['governanceStaking'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + beforeTreasuryBal = dai.balanceOf(treasuryVault.address) + beforeGovStakingBal = dai.balanceOf(governanceStaking) + + governanceStaking.setRewardDistribution(treasuryVault.address, sender) + + treasuryVault.toGovernanceStaking(sender) + + afterTreasuryBal = dai.balanceOf(treasuryVault.address) + afterGovStakingBal = dai.balanceOf(governanceStaking) + + afterGovStakingBal == beforeGovStakingBal + beforeTreasuryBal + afterTreasuryBal == 0 + + +################################## Governance DAO tests ##################################### +# TODO: add Governance DAO tests + +def test_governance_voter_register(deployedContracts): + governanceStaking = deployedContracts['governanceStaking'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + governanceStaking.register(sender) + + assert governanceStaking.voters(DEFAULT_DEPLOYER_ACCOUNT) == True + + +def test_governance_voter_revoke(deployedContracts): + governanceStaking = deployedContracts['governanceStaking'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + governanceStaking.revoke(sender) + + assert governanceStaking.voters(DEFAULT_DEPLOYER_ACCOUNT) == False + + +def test_governance_stake(deployedContracts): + governanceStaking = deployedContracts['governanceStaking'] + sender = deployedContracts['sender'] + sdtToken = deployedContracts['sdtToken'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + sdtSupply = sdtToken.totalSupply() + + governanceStaking.register(sender) + + sdtToken.approve(governanceStaking.address, sdtSupply, sender) + + assert sdtToken.allowance(DEFAULT_DEPLOYER_ACCOUNT, + governanceStaking.address) == sdtSupply + + governanceStaking.stake(sdtSupply, sender) + + assert governanceStaking.votes(DEFAULT_DEPLOYER_ACCOUNT) == sdtSupply + assert governanceStaking.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) == sdtSupply + + +def test_governance_proposal(deployedContracts): + governanceStaking = deployedContracts['governanceStaking'] + sender = deployedContracts['sender'] + daiBal = deployedContracts['daiBal'] + dai = deployedContracts['dai'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + EXECUTOR = accounts[9].address + + governanceStaking.setRewardDistribution(DEFAULT_DEPLOYER_ACCOUNT, sender) + + dai.approve(governanceStaking.address, daiBal, sender) + governanceStaking.notifyRewardAmount(daiBal, sender) + + governanceStaking.propose(EXECUTOR, "link to proposal", sender) + + proposal = governanceStaking.proposals(0) + + assert proposal[0] == 0 + assert proposal[1] == DEFAULT_DEPLOYER_ACCOUNT + assert proposal[2] == 0 + assert proposal[3] == 0 + assert proposal[4] == web3.eth.blockNumber + assert proposal[5] == governanceStaking.period() + web3.eth.blockNumber + assert proposal[6] == EXECUTOR + assert proposal[7] == "link to proposal" + assert proposal[8] == governanceStaking.votes(DEFAULT_DEPLOYER_ACCOUNT) + assert proposal[9] == 0 + assert proposal[10] == governanceStaking.quorum() + assert proposal[11] == True + + +def test_governance_getReward(deployedContracts): + + governanceStaking = deployedContracts['governanceStaking'] + sender = deployedContracts['sender'] + daiBal = deployedContracts['daiBal'] + dai = deployedContracts['dai'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + assert governanceStaking.rewardPerToken() > 0 + assert governanceStaking.earned(DEFAULT_DEPLOYER_ACCOUNT) > 0 + assert dai.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) == 0 + + governanceStaking.getReward(sender) + + assert dai.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) > 0 + + governanceStakingBal = dai.balanceOf(governanceStaking.address) + senderBal = dai.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + + # assert daiBal == governanceStakingBal + senderBal + + +def test_governance_voteFor(deployedContracts): + governanceStaking = deployedContracts['governanceStaking'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + votes = governanceStaking.votes(DEFAULT_DEPLOYER_ACCOUNT) + + beforeFor = governanceStaking.proposals(0)[2] + + governanceStaking.voteFor(0, sender) + + afterFor = governanceStaking.proposals(0)[2] + + assert afterFor == beforeFor + votes + + +def test_governance_voteAgainst(deployedContracts): + governanceStaking = deployedContracts['governanceStaking'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + votes = governanceStaking.votes(DEFAULT_DEPLOYER_ACCOUNT) + + beforeAgainst = governanceStaking.proposals(0)[3] + + governanceStaking.voteAgainst(0, sender) + + afterAgainst = governanceStaking.proposals(0)[3] + + assert afterAgainst == beforeAgainst + votes + + +def test_governance_withdraw(deployedContracts): + governanceStaking = deployedContracts['governanceStaking'] + sender = deployedContracts['sender'] + sdtToken = deployedContracts['sdtToken'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + totalSupply = sdtToken.totalSupply() + + chain.mine(governanceStaking.voteLock( + DEFAULT_DEPLOYER_ACCOUNT) - web3.eth.blockNumber + 1) + + beforeBal = sdtToken.balanceOf(governanceStaking.address) + + governanceStaking.withdraw(beforeBal, sender) + + afterBal = sdtToken.balanceOf(governanceStaking.address) + + assert totalSupply == beforeBal + afterBal + + +def test_governance_seize(deployedContracts): + governanceStaking = deployedContracts['governanceStaking'] + sender = deployedContracts['sender'] + sdtToken = deployedContracts['sdtToken'] + threeCrv = deployedContracts['threeCrv'] + governanceGSPAddress = deployedContracts['governanceGSPAddress'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + threeCrv.transfer(governanceStaking.address, 100, sender) + + gspBalBefore = threeCrv.balanceOf(governanceGSPAddress) + govStakingBalBefore = threeCrv.balanceOf(governanceStaking) + + governanceStaking.seize(threeCrv, 100, {'from': governanceGSPAddress}) + + gspBalAfter = threeCrv.balanceOf(governanceGSPAddress) + govStakingBalAfter = threeCrv.balanceOf(governanceStaking) + + assert gspBalAfter == gspBalBefore + 100 + assert govStakingBalAfter == govStakingBalBefore - 100 + + +################################## MasterChef tests ##################################### + + +def test_MasterChef_add(deployedContracts): + masterchef = deployedContracts['masterchef'] + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + + masterchef.add(100, threePoolVault, False, sender) + + poolInfo = masterchef.poolInfo(0) + + assert poolInfo[0] == threePoolVault.address + assert poolInfo[1] == 100 + assert poolInfo[2] == web3.eth.blockNumber + assert poolInfo[3] == 0 + + +def test_MasterChef_set(deployedContracts): + masterchef = deployedContracts['masterchef'] + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + + masterchef.set(0, 1000, False, sender) + + poolInfo = masterchef.poolInfo(0) + + assert poolInfo[0] == threePoolVault.address + assert poolInfo[1] == 1000 + assert poolInfo[2] == web3.eth.blockNumber - 1 + assert poolInfo[3] == 0 + + +def test_MasterChef_setSdtPerBlock(deployedContracts): + masterchef = deployedContracts['masterchef'] + sender = deployedContracts['sender'] + + masterchef.setSdtPerBlock(2000, sender) + + assert masterchef.sdtPerBlock() == 2000 + + +def test_MasterChef_setBonusEndBlock(deployedContracts): + masterchef = deployedContracts['masterchef'] + sender = deployedContracts['sender'] + + blockNum = web3.eth.blockNumber + 1000 + + masterchef.setBonusEndBlock(blockNum) + + assert masterchef.bonusEndBlock() == blockNum + + +def test_MasterChef_setDevFundDivRate(deployedContracts): + masterchef = deployedContracts['masterchef'] + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + + masterchef.setDevFundDivRate(60) + + assert masterchef.devFundDivRate() == 60 + + +def test_MasterChef_deposit(deployedContracts): + masterchef = deployedContracts['masterchef'] + threePoolVault = deployedContracts['threePoolVault'] + threeCrv = deployedContracts['threeCrv'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + senderBal = threeCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + + threePoolVault.deposit(1000, sender) + + threePoolVault.approve(masterchef.address, 1000, sender) + + masterchefBalBefore = threePoolVault.balanceOf(masterchef.address) + senderBalBefore = threePoolVault.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + + masterchef.deposit(0, 1000, sender) + + masterchefBalAfter = threePoolVault.balanceOf(masterchef.address) + senderBalAfter = threePoolVault.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + + assert masterchefBalAfter == masterchefBalBefore + 1000 + assert senderBalAfter == senderBalBefore - 1000 + + +def test_MasterChef_updatePool(deployedContracts): + masterchef = deployedContracts['masterchef'] + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + sdtToken = deployedContracts['sdtToken'] + + assert sdtToken.totalSupply() == 1000000000000000000000 + assert masterchef.poolInfo(0)[3] == 0 + + masterchef.updatePool(0, sender) + + assert sdtToken.totalSupply() > 1000000000000000000000 + assert masterchef.poolInfo(0)[3] > 0 + + +def test_MasterChef_withdraw(deployedContracts): + masterchef = deployedContracts['masterchef'] + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + sdtToken = deployedContracts['sdtToken'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + + chain.mine(1) + + masterchefBalBefore = threePoolVault.balanceOf(masterchef.address) + senderBalBefore = threePoolVault.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + + assert sdtToken.balanceOf( + DEFAULT_DEPLOYER_ACCOUNT) == 1000000000000000000000 + + masterchef.withdraw(0, 1000, sender) + + assert sdtToken.balanceOf( + DEFAULT_DEPLOYER_ACCOUNT) > 1000000000000000000000 + + masterchefBalAfter = threePoolVault.balanceOf(masterchef.address) + senderBalAfter = threePoolVault.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + + assert masterchefBalAfter == masterchefBalBefore - 1000 + assert senderBalAfter == senderBalBefore + 1000 + + +def test_MasterChef_transferOwnership(deployedContracts): + masterchef = deployedContracts['masterchef'] + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + sdtToken = deployedContracts['sdtToken'] + timelockGovernance = deployedContracts['timelockGovernance'] + + masterchef.transferOwnership(timelockGovernance.address, sender) + + assert masterchef.owner() == timelockGovernance.address + + # add check that onlyOwner functions should fail if trx is sent via DEFAULT_DEPLOYER_ACCOUNT + + +################################## eursCrvVault tests ##################################### + + +def test_eursCrvVault_token(deployedContracts): + """ + Returns the unwrapped native token address that the Vault takes as deposit. + """ + eursCrvVault = deployedContracts['eursCrvVault'] + eursCrv = deployedContracts['eursCrv'] + + token = eursCrvVault.token() + + assert token == eursCrv.address + + +def test_eursCrvVault_name(deployedContracts): + """ + Returns the vault’s wrapped token name as a string, e.g. “yearn Dai Stablecoin". + """ + eursCrvVault = deployedContracts['eursCrvVault'] + eursCrv = deployedContracts['eursCrv'] + + name = eursCrvVault.name() + eursCrvName = eursCrv.name() + + assert name == "stake dao " + eursCrvName + + +def test_eursCrvVault_symbol(deployedContracts): + """ + Returns the vault’s wrapped token symbol as a string, e.g. “yDai”. + """ + eursCrvVault = deployedContracts['eursCrvVault'] + eursCrv = deployedContracts['eursCrv'] + + symbol = eursCrvVault.symbol() + eursCrvSymbol = eursCrv.symbol() + + assert symbol == "sd" + eursCrvSymbol + + +def test_eursCrvVault_decimals(deployedContracts): + """ + Returns the amount of decimals for this vault’s wrapped token as a uint8. + """ + eursCrvVault = deployedContracts['eursCrvVault'] + eursCrv = deployedContracts['eursCrv'] + + decimals = eursCrvVault.decimals() + eursCrvDecimals = eursCrv.decimals() + + assert decimals == eursCrvDecimals + + +def test_eursCrvVault_controller(deployedContracts): + """ + Returns the address of the Vault's Controller. + """ + eursCrvVault = deployedContracts['eursCrvVault'] + controller = deployedContracts['controller'] + + threePoolVaultController = eursCrvVault.controller() + + assert controller.address == threePoolVaultController + + +def test_eursCrvVault_governance(deployedContracts): + """ + Returns the address of the Vault’s governance contract. + """ + eursCrvVault = deployedContracts['eursCrvVault'] + governanceGSPAddress = deployedContracts['governanceGSPAddress'] + + threePoolVaultGovernance = eursCrvVault.governance() + + assert threePoolVaultGovernance == governanceGSPAddress + + +def test_eursCrvVault_deposit(deployedContracts): + """ + Deposits token (same as want() returns) into a smart contact specified by the Strategy. + """ + eursCrvVault = deployedContracts['eursCrvVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + eursCrv = deployedContracts['eursCrv'] + + beforeSenderBal = eursCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = eursCrv.balanceOf(eursCrvVault.address) + + eursCrvVault.deposit(1, sender) + + afterSenderBal = eursCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = eursCrv.balanceOf(eursCrvVault.address) + + assert afterSenderBal == beforeSenderBal - 1 + assert afterVaultBal == beforeVaultBal + 1 + + +def test_eursCrvVault_depositAll(deployedContracts): + """ + Deposits token (same as want() returns) into a smart contact specified by the Strategy. + """ + eursCrvVault = deployedContracts['eursCrvVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + eursCrv = deployedContracts['eursCrv'] + + beforeSenderBal = eursCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = eursCrv.balanceOf(eursCrvVault.address) + + eursCrvVault.deposit(beforeSenderBal, sender) + + afterSenderBal = eursCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = eursCrv.balanceOf(eursCrvVault.address) + + assert afterSenderBal == 0 + assert afterVaultBal == beforeVaultBal + beforeSenderBal + + +def test_eursCrvVault_getPricePerFullShare(deployedContracts): + """ + Returns the price of the Vault’s wrapped token, denominated in the unwrapped native token. + The calculation is: nativeTokenBalance/yTokenTotalSupply + + Where nativeTokenBalance is the current balance of native token (e.g. DAI) in the Vault, + Controller and Strategy contracts. And yTokenTotalSupply is the total supply of the Vault's + wrapped Token (e.g. yDAI). + """ + eursCrvVault = deployedContracts['eursCrvVault'] + pricePerFullShare = eursCrvVault.getPricePerFullShare() + + # TODO: Calculate it via above formula and then assert + assert pricePerFullShare == 1000000000000000000 + + +def test_eursCrvVault_withdraw(deployedContracts): + """ + Partially withdraws funds (denominated in want() token) from the Strategy, + and should always only be sending these to the Vault. In case the Strategy + implements harvest(), a withdrawal fee may be applied. This function should + have access control enforcing the Controller only to be its allowed caller. + """ + eursCrvVault = deployedContracts['eursCrvVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + eursCrv = deployedContracts['eursCrv'] + + beforeSenderBal = eursCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = eursCrv.balanceOf(eursCrvVault.address) + + eursCrvVault.withdraw(1, sender) + + afterSenderBal = eursCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = eursCrv.balanceOf(eursCrvVault.address) + + assert afterSenderBal == beforeSenderBal + 1 + assert afterVaultBal == beforeVaultBal - 1 + + +def test_eursCrvVault_withdrawAll(deployedContracts): + """ + Withdraws the entire amount of want() tokens available, + and should always only be sending these to the Vault. This + function should have access control enforcing the Controller + only to be its allowed caller. Typically used when migrating + strategies. + + The function typically uses withdraw() and performs a set of + sequential function calls depending on the Strategy. + + If the Strategy implements liquidity pools or lending platforms, + then withdrawal from these platforms should be performed until + the Vault’s unwrapped token is delivered back to the vault. + + Returns a uint256 of the total amount withdrawn. + """ + eursCrvVault = deployedContracts['eursCrvVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + eursCrv = deployedContracts['eursCrv'] + + beforeSenderBal = eursCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = eursCrv.balanceOf(eursCrvVault.address) + + eursCrvVault.withdrawAll(sender) + + afterSenderBal = eursCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = eursCrv.balanceOf(eursCrvVault.address) + + assert afterVaultBal == 0 + assert afterSenderBal == beforeSenderBal + beforeVaultBal + + +def test_eursCrvVault_earn(deployedContracts): + eursCrvVault = deployedContracts['eursCrvVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + eursCrv = deployedContracts['eursCrv'] + + eursCrvBal = eursCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + + eursCrv.approve(eursCrvVault.address, eursCrvBal, sender) + eursCrvVault.deposit(eursCrvBal, sender) + available = eursCrvVault.available() + + assert eursCrv.balanceOf(eursCrvVault.address) == eursCrvBal + + beforeVaultBal = eursCrv.balanceOf(eursCrvVault.address) + + eursCrvVault.earn(sender) + + afterVaultBal = eursCrv.balanceOf(eursCrvVault.address) + + assert afterVaultBal == beforeVaultBal - available + + +################################## strategyCurveEursCrvVoterProxy tests ##################################### + +def test_strategyCurveEursCrvVoterProxy_want(deployedContracts): + """ + Returns the address of the unwrapped token that the Strategy takes as deposit. + """ + + strategyCurveEursCrvVoterProxy = deployedContracts['strategyCurveEursCrvVoterProxy'] + eursCrv = deployedContracts['eursCrv'] + + want = strategyCurveEursCrvVoterProxy.want() + + assert want == eursCrv.address + + +def test_strategyCurveEursCrvVoterProxy_harvest(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts['strategyCurveEursCrvVoterProxy'] + STRATEGIST = deployedContracts['STRATEGIST'] + + strategyCurveEursCrvVoterProxy.harvest({'from': STRATEGIST}) + + +################################## threePoolVault tests ##################################### + + +def test_threePoolVault_token(deployedContracts): + """ + Returns the unwrapped native token address that the Vault takes as deposit. + """ + threePoolVault = deployedContracts['threePoolVault'] + threeCrv = deployedContracts['threeCrv'] + + token = threePoolVault.token() + + assert token == threeCrv.address + + +def test_threePoolVault_name(deployedContracts): + """ + Returns the vault’s wrapped token name as a string, e.g. “yearn Dai Stablecoin". + """ + threePoolVault = deployedContracts['threePoolVault'] + threeCrv = deployedContracts['threeCrv'] + + name = threePoolVault.name() + threeCrvName = threeCrv.name() + + assert name == "stake dao " + threeCrvName + + +def test_threePoolVault_symbol(deployedContracts): + """ + Returns the vault’s wrapped token symbol as a string, e.g. “yDai”. + """ + threePoolVault = deployedContracts['threePoolVault'] + threeCrv = deployedContracts['threeCrv'] + + symbol = threePoolVault.symbol() + threeCrvSymbol = threeCrv.symbol() + + assert symbol == "sd" + threeCrvSymbol + + +def test_threePoolVault_decimals(deployedContracts): + """ + Returns the amount of decimals for this vault’s wrapped token as a uint8. + """ + threePoolVault = deployedContracts['threePoolVault'] + threeCrv = deployedContracts['threeCrv'] + + decimals = threePoolVault.decimals() + threeCrvDecimals = threeCrv.decimals() + + assert decimals == threeCrvDecimals + + +def test_threePoolVault_controller(deployedContracts): + """ + Returns the address of the Vault's Controller. + """ + threePoolVault = deployedContracts['threePoolVault'] + controller = deployedContracts['controller'] + + threePoolVaultController = threePoolVault.controller() + + assert controller.address == threePoolVaultController + + +def test_threePoolVault_governance(deployedContracts): + """ + Returns the address of the Vault’s governance contract. + """ + threePoolVault = deployedContracts['threePoolVault'] + governanceGSPAddress = deployedContracts['governanceGSPAddress'] + + threePoolVaultGovernance = threePoolVault.governance() + + assert threePoolVaultGovernance == governanceGSPAddress + + +def test_threePoolVault_deposit(deployedContracts): + """ + Deposits token (same as want() returns) into a smart contact specified by the Strategy. + """ + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + threeCrv = deployedContracts['threeCrv'] + + beforeSenderBal = threeCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = threeCrv.balanceOf(threePoolVault.address) + + threePoolVault.deposit(1, sender) + + afterSenderBal = threeCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = threeCrv.balanceOf(threePoolVault.address) + + assert afterSenderBal == beforeSenderBal - 1 + assert afterVaultBal == beforeVaultBal + 1 + + +def test_threePoolVault_depositAll(deployedContracts): + """ + Deposits token (same as want() returns) into a smart contact specified by the Strategy. + """ + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + threeCrv = deployedContracts['threeCrv'] + + beforeSenderBal = threeCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = threeCrv.balanceOf(threePoolVault.address) + + threePoolVault.deposit(beforeSenderBal, sender) + + afterSenderBal = threeCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = threeCrv.balanceOf(threePoolVault.address) + + assert afterSenderBal == 0 + assert afterVaultBal == beforeVaultBal + beforeSenderBal + + +def test_threePoolVault_getPricePerFullShare(deployedContracts): + """ + Returns the price of the Vault’s wrapped token, denominated in the unwrapped native token. + The calculation is: nativeTokenBalance/yTokenTotalSupply + + Where nativeTokenBalance is the current balance of native token (e.g. DAI) in the Vault, + Controller and Strategy contracts. And yTokenTotalSupply is the total supply of the Vault's + wrapped Token (e.g. yDAI). + """ + threePoolVault = deployedContracts['threePoolVault'] + pricePerFullShare = threePoolVault.getPricePerFullShare() + + # TODO: Calculate it via above formula and then assert + assert pricePerFullShare == 1000000000000000000 + + +def test_threePoolVault_withdraw(deployedContracts): + """ + Partially withdraws funds (denominated in want() token) from the Strategy, + and should always only be sending these to the Vault. In case the Strategy + implements harvest(), a withdrawal fee may be applied. This function should + have access control enforcing the Controller only to be its allowed caller. + """ + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + threeCrv = deployedContracts['threeCrv'] + + beforeSenderBal = threeCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = threeCrv.balanceOf(threePoolVault.address) + + threePoolVault.withdraw(1, sender) + + afterSenderBal = threeCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = threeCrv.balanceOf(threePoolVault.address) + + assert afterSenderBal == beforeSenderBal + 1 + assert afterVaultBal == beforeVaultBal - 1 + + +def test_threePoolVault_withdrawAll(deployedContracts): + """ + Withdraws the entire amount of want() tokens available, + and should always only be sending these to the Vault. This + function should have access control enforcing the Controller + only to be its allowed caller. Typically used when migrating + strategies. + + The function typically uses withdraw() and performs a set of + sequential function calls depending on the Strategy. + + If the Strategy implements liquidity pools or lending platforms, + then withdrawal from these platforms should be performed until + the Vault’s unwrapped token is delivered back to the vault. + + Returns a uint256 of the total amount withdrawn. + """ + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + threeCrv = deployedContracts['threeCrv'] + + beforeSenderBal = threeCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = threeCrv.balanceOf(threePoolVault.address) + + threePoolVault.withdrawAll(sender) + + afterSenderBal = threeCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = threeCrv.balanceOf(threePoolVault.address) + + assert afterVaultBal == 0 + assert afterSenderBal == beforeSenderBal + beforeVaultBal + + +def test_threePoolVault_earn(deployedContracts): + threePoolVault = deployedContracts['threePoolVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + threeCrv = deployedContracts['threeCrv'] + + threeCrvBal = threeCrv.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + + threeCrv.approve(threePoolVault.address, threeCrvBal, sender) + threePoolVault.deposit(threeCrvBal, sender) + available = threePoolVault.available() + + assert threeCrv.balanceOf(threePoolVault.address) == threeCrvBal + + beforeVaultBal = threeCrv.balanceOf(threePoolVault.address) + + threePoolVault.earn(sender) + + afterVaultBal = threeCrv.balanceOf(threePoolVault.address) + + assert afterVaultBal == beforeVaultBal - available + + +################################## strategyCurve3CrvVoterProxy tests ##################################### + +def test_strategyCurve3CrvVoterProxy_want(deployedContracts): + """ + Returns the address of the unwrapped token that the Strategy takes as deposit. + """ + + strategyCurve3CrvVoterProxy = deployedContracts['strategyCurve3CrvVoterProxy'] + threeCrv = deployedContracts['threeCrv'] + + want = strategyCurve3CrvVoterProxy.want() + + assert want == threeCrv.address + + +def test_strategyCurve3CrvVoterProxy_harvest(deployedContracts): + strategyCurve3CrvVoterProxy = deployedContracts['strategyCurve3CrvVoterProxy'] + STRATEGIST = deployedContracts['STRATEGIST'] + + strategyCurve3CrvVoterProxy.harvest({'from': STRATEGIST}) + + +################################## sbtcVault tests ##################################### + + +def test_sbtcVault_token(deployedContracts): + """ + Returns the unwrapped native token address that the Vault takes as deposit. + """ + sbtcVault = deployedContracts['sbtcVault'] + sbtc = deployedContracts['sbtc'] + + token = sbtcVault.token() + + assert token == sbtc.address + + +def test_sbtcVault_name(deployedContracts): + """ + Returns the vault’s wrapped token name as a string, e.g. “yearn Dai Stablecoin". + """ + sbtcVault = deployedContracts['sbtcVault'] + sbtc = deployedContracts['sbtc'] + + name = sbtcVault.name() + sbtcName = sbtc.name() + + assert name == "stake dao " + sbtcName + + +def test_sbtcVault_symbol(deployedContracts): + """ + Returns the vault’s wrapped token symbol as a string, e.g. “yDai”. + """ + sbtcVault = deployedContracts['sbtcVault'] + sbtc = deployedContracts['sbtc'] + + symbol = sbtcVault.symbol() + sbtcSymbol = sbtc.symbol() + + assert symbol == "sd" + sbtcSymbol + + +def test_sbtcVault_decimals(deployedContracts): + """ + Returns the amount of decimals for this vault’s wrapped token as a uint8. + """ + sbtcVault = deployedContracts['sbtcVault'] + sbtc = deployedContracts['sbtc'] + + decimals = sbtcVault.decimals() + sbtcDecimals = sbtc.decimals() + + assert decimals == sbtcDecimals + + +def test_sbtcVault_controller(deployedContracts): + """ + Returns the address of the Vault's Controller. + """ + sbtcVault = deployedContracts['sbtcVault'] + controller = deployedContracts['controller'] + + sbtcVaultController = sbtcVault.controller() + + assert controller.address == sbtcVaultController + + +def test_sbtcVault_governance(deployedContracts): + """ + Returns the address of the Vault’s governance contract. + """ + sbtcVault = deployedContracts['sbtcVault'] + governanceGSPAddress = deployedContracts['governanceGSPAddress'] + + sbtcVaultGovernance = sbtcVault.governance() + + assert sbtcVaultGovernance == governanceGSPAddress + + +def test_sbtcVault_deposit(deployedContracts): + """ + Deposits token (same as want() returns) into a smart contact specified by the Strategy. + """ + sbtcVault = deployedContracts['sbtcVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + sbtc = deployedContracts['sbtc'] + + beforeSenderBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = sbtc.balanceOf(sbtcVault.address) + + sbtcVault.deposit(1, sender) + + afterSenderBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = sbtc.balanceOf(sbtcVault.address) + + assert afterSenderBal == beforeSenderBal - 1 + assert afterVaultBal == beforeVaultBal + 1 + + +def test_sbtcVault_depositAll(deployedContracts): + """ + Deposits token (same as want() returns) into a smart contact specified by the Strategy. + """ + sbtcVault = deployedContracts['sbtcVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + sbtc = deployedContracts['sbtc'] + + beforeSenderBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = sbtc.balanceOf(sbtcVault.address) + + sbtcVault.deposit(beforeSenderBal, sender) + + afterSenderBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = sbtc.balanceOf(sbtcVault.address) + + assert afterSenderBal == 0 + assert afterVaultBal == beforeVaultBal + beforeSenderBal + + +def test_sbtcVault_getPricePerFullShare(deployedContracts): + """ + Returns the price of the Vault’s wrapped token, denominated in the unwrapped native token. + The calculation is: nativeTokenBalance/yTokenTotalSupply + + Where nativeTokenBalance is the current balance of native token (e.g. DAI) in the Vault, + Controller and Strategy contracts. And yTokenTotalSupply is the total supply of the Vault's + wrapped Token (e.g. yDAI). + """ + sbtcVault = deployedContracts['sbtcVault'] + pricePerFullShare = sbtcVault.getPricePerFullShare() + + # TODO: Calculate it via above formula and then assert + assert pricePerFullShare == 1000000000000000000 + + +def test_sbtcVault_withdraw(deployedContracts): + """ + Partially withdraws funds (denominated in want() token) from the Strategy, + and should always only be sending these to the Vault. In case the Strategy + implements harvest(), a withdrawal fee may be applied. This function should + have access control enforcing the Controller only to be its allowed caller. + """ + sbtcVault = deployedContracts['sbtcVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + sbtc = deployedContracts['sbtc'] + + beforeSenderBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = sbtc.balanceOf(sbtcVault.address) + + sbtcVault.withdraw(1, sender) + + afterSenderBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = sbtc.balanceOf(sbtcVault.address) + + assert afterSenderBal == beforeSenderBal + 1 + assert afterVaultBal == beforeVaultBal - 1 + + +def test_sbtcVault_withdrawAll(deployedContracts): + """ + Withdraws the entire amount of want() tokens available, + and should always only be sending these to the Vault. This + function should have access control enforcing the Controller + only to be its allowed caller. Typically used when migrating + strategies. + + The function typically uses withdraw() and performs a set of + sequential function calls depending on the Strategy. + + If the Strategy implements liquidity pools or lending platforms, + then withdrawal from these platforms should be performed until + the Vault’s unwrapped token is delivered back to the vault. + + Returns a uint256 of the total amount withdrawn. + """ + sbtcVault = deployedContracts['sbtcVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + sbtc = deployedContracts['sbtc'] + + beforeSenderBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + beforeVaultBal = sbtc.balanceOf(sbtcVault.address) + + sbtcVault.withdrawAll(sender) + + afterSenderBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + afterVaultBal = sbtc.balanceOf(sbtcVault.address) + + assert afterVaultBal == 0 + assert afterSenderBal == beforeSenderBal + beforeVaultBal + + +def test_sbtcVault_earn(deployedContracts): + sbtcVault = deployedContracts['sbtcVault'] + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + sbtc = deployedContracts['sbtc'] + + sbtcBal = sbtc.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + + sbtc.approve(sbtcVault.address, sbtcBal, sender) + sbtcVault.deposit(sbtcBal, sender) + available = sbtcVault.available() + + assert sbtc.balanceOf(sbtcVault.address) == sbtcBal + + beforeVaultBal = sbtc.balanceOf(sbtcVault.address) + + sbtcVault.earn(sender) + + afterVaultBal = sbtc.balanceOf(sbtcVault.address) + + assert afterVaultBal == beforeVaultBal - available + + +################################## StrategyCurveBTCVoterProxy tests ##################################### + + +def test_strategyCurveBTCVoterProxy_want(deployedContracts): + """ + Returns the address of the unwrapped token that the Strategy takes as deposit. + """ + + strategyCurveBTCVoterProxy = deployedContracts['strategyCurveBTCVoterProxy'] + sbtc = deployedContracts['sbtc'] + + want = strategyCurveBTCVoterProxy.want() + + assert want == sbtc.address + + +def test_strategyCurveBTCVoterProxy_harvest(deployedContracts): + strategyCurveBTCVoterProxy = deployedContracts['strategyCurveBTCVoterProxy'] + STRATEGIST = deployedContracts['STRATEGIST'] + + strategyCurveBTCVoterProxy.harvest({'from': STRATEGIST}) + + +################################## Controller tests ##################################### + +def test_controller_inCaseStrategyTokenGetStuck_strategyCurve3CrvVoterProxy(deployedContracts): + """ + This controller function calls withdraw(IERC20 _asset) + + withdraw(IERC20 _asset): + + Dust collecting function to create additional rewards out of + tokens that were incorrectly sent to the Strategy. + + Takes an ERC20 token address and should send the full amount + of any such tokens in the Strategy to the Controller. + + This function should have access control enforcing the Controller + only to be its allowed caller, and checks in place to ensure that + the token types to withdraw are not those used by the Strategy. + """ + strategyCurve3CrvVoterProxy = deployedContracts['strategyCurve3CrvVoterProxy'] + controller = deployedContracts['controller'] + sbtc = deployedContracts['sbtc'] + sender = deployedContracts['sender'] + STRATEGIST = deployedContracts['STRATEGIST'] + + # Send some sBTC token to the strategyCurve3CrvVoterProxy + sbtc.transfer(strategyCurve3CrvVoterProxy.address, 1, sender) + + balBefore = sbtc.balanceOf(controller.address) + + # Get the token out of strategyCurve3CrvVoterProxy to Controller + controller.inCaseStrategyTokenGetStuck( + strategyCurve3CrvVoterProxy.address, sbtc.address, {'from': STRATEGIST}) + + balAfter = sbtc.balanceOf(controller.address) + + assert balAfter == balBefore + 1 + + +def test_controller_inCaseStrategyTokenGetStuck_strategyCurveEursCrvVoterProxy(deployedContracts): + """ + This controller function calls withdraw(IERC20 _asset) + + withdraw(IERC20 _asset): + + Dust collecting function to create additional rewards out of + tokens that were incorrectly sent to the Strategy. + + Takes an ERC20 token address and should send the full amount + of any such tokens in the Strategy to the Controller. + + This function should have access control enforcing the Controller + only to be its allowed caller, and checks in place to ensure that + the token types to withdraw are not those used by the Strategy. + """ + strategyCurveEursCrvVoterProxy = deployedContracts['strategyCurveEursCrvVoterProxy'] + controller = deployedContracts['controller'] + sbtc = deployedContracts['sbtc'] + sender = deployedContracts['sender'] + STRATEGIST = deployedContracts['STRATEGIST'] + + # Send some sBTC token to the strategyCurveEursCrvVoterProxy + sbtc.transfer(strategyCurveEursCrvVoterProxy.address, 1, sender) + + balBefore = sbtc.balanceOf(controller.address) + + # Get the token out of strategyCurveEursCrvVoterProxy to Controller + controller.inCaseStrategyTokenGetStuck( + strategyCurveEursCrvVoterProxy.address, sbtc.address, {'from': STRATEGIST}) + + balAfter = sbtc.balanceOf(controller.address) + + assert balAfter == balBefore + 1 + + +def test_controller_inCaseStrategyTokenGetStuck_strategyCurveBTCVoterProxy(deployedContracts): + """ + This controller function calls withdraw(IERC20 _asset) + + withdraw(IERC20 _asset): + + Dust collecting function to create additional rewards out of + tokens that were incorrectly sent to the Strategy. + + Takes an ERC20 token address and should send the full amount + of any such tokens in the Strategy to the Controller. + + This function should have access control enforcing the Controller + only to be its allowed caller, and checks in place to ensure that + the token types to withdraw are not those used by the Strategy. + """ + strategyCurveBTCVoterProxy = deployedContracts['strategyCurveBTCVoterProxy'] + controller = deployedContracts['controller'] + threeCrv = deployedContracts['threeCrv'] + sender = deployedContracts['sender'] + STRATEGIST = deployedContracts['STRATEGIST'] + + # Send some threeCrv token to the strategyCurveBTCVoterProxy + threeCrv.transfer(strategyCurveBTCVoterProxy.address, 1, sender) + + balBefore = threeCrv.balanceOf(controller.address) + + # Get the token out of strategyCurveBTCVoterProxy to Controller + controller.inCaseStrategyTokenGetStuck( + strategyCurveBTCVoterProxy.address, threeCrv.address, {'from': STRATEGIST}) + + balAfter = threeCrv.balanceOf(controller.address) + + assert balAfter == balBefore + 1 + + +################################## veCurveVault tests ##################################### + +def test_veCurveVault_deposit(deployedContracts): + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + crv = deployedContracts['crv'] + crvBal = deployedContracts['crvBal'] + veCurve = deployedContracts['veCurve'] + + crv.approve(veCurve.address, crvBal, sender) + + veCurve.depositAll(sender) + + depositBal = veCurve.balanceOf(DEFAULT_DEPLOYER_ACCOUNT) + + assert crvBal == depositBal + + +def test_veCurveVault_claim(deployedContracts): + sender = deployedContracts['sender'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + veCurve = deployedContracts['veCurve'] + + veCurve.claim(sender) diff --git a/protocol/tests/dev/test_dev.py b/protocol/tests/dev/test_dev.py new file mode 100644 index 0000000..a54b049 --- /dev/null +++ b/protocol/tests/dev/test_dev.py @@ -0,0 +1,279 @@ +import pytest +from brownie import * +import typer +import json + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts[0] + + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + TREASURY_VAULT = deployed[ENV]['TREASURY_VAULT'] + treasuryVault = Contract(TREASURY_VAULT) + + SBTC_CRV = deployed[ENV]['SBTC_CRV'] + + THREE_CRV = deployed[ENV]['THREE_CRV'] + + EURS_CRV = deployed[ENV]['EURS_CRV'] + + DAI = deployed[ENV]['DAI'] + dai = Contract(DAI) + + VESTING_ESCROW = deployed[ENV]['VESTING_ESCROW'] + + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + multisig = {'from': GNOSIS_SAFE_PROXY} + + controller = Controller.at(deployed[ENV]['CONTROLLER']) + + DEV = '0x5cAf454Ba92e6F2c929DF14667Ee360eD9fD5b26' + + sender = {'from': '0x9f535E3c63Cd1447164BED4933d1efefBbC97a3f'} + + dev = Contract(DEV) + + devVault = yVault.at('0xd072d3dB1343029087cb9466DE19492b7A1C66FF') + + devStrategy = StrategyBunchyDev.at( + '0x7D3b52A6ae25545f825bc230f7011Fc97e821911') + + return {"dai": dai, "treasuryVault": treasuryVault, "multisig": multisig, "deployer": deployer, "DAI": DAI, "dev": dev, "SBTC_CRV": SBTC_CRV, "THREE_CRV": THREE_CRV, "EURS_CRV": EURS_CRV, "devVault": devVault, "devStrategy": devStrategy, "sender": sender, "controller": controller} + + +def test_dev_vault_deposit(deployedContracts): + dev = deployedContracts['dev'] + devVault = deployedContracts['devVault'] + devStrategy = deployedContracts['devStrategy'] + sender = deployedContracts['sender'] + + shareBefore = devVault.balanceOf(sender['from']) + senderDevBefore = dev.balanceOf(sender['from']) + vaultDevBefore = dev.balanceOf(devVault.address) + + dev.approve(devVault.address, dev.balanceOf(sender['from']), sender) + devVault.deposit(dev.balanceOf(sender['from']), sender) + + shareAfter = devVault.balanceOf(sender['from']) + senderDevAfter = dev.balanceOf(sender['from']) + vaultDevAfter = dev.balanceOf(devVault.address) + + print("Minted shares", (shareAfter - shareBefore) / 10**18) + + assert shareAfter - shareBefore > 0 + assert senderDevBefore - senderDevAfter > 0 + assert senderDevBefore - senderDevAfter == vaultDevAfter - vaultDevBefore + + +def test_dev_vault_earn(deployedContracts): + deployer = deployedContracts['deployer'] + dev = deployedContracts['dev'] + devVault = deployedContracts['devVault'] + devStrategy = deployedContracts['devStrategy'] + sender = deployedContracts['sender'] + property = devStrategy.property() + + vaultDevBefore = dev.balanceOf(devVault.address) + propertyDevBefore = dev.balanceOf(property) + + devVault.earn(deployer) + + vaultDevAfter = dev.balanceOf(devVault.address) + propertyDevAfter = dev.balanceOf(property) + + print("Vault DEV decrease", (vaultDevBefore - vaultDevAfter) / 10**18) + + assert vaultDevBefore - vaultDevAfter > 0 + assert vaultDevBefore - vaultDevAfter == propertyDevAfter - propertyDevBefore + + +def test_dev_strategy_harvest(deployedContracts): + deployer = deployedContracts['deployer'] + dev = deployedContracts['dev'] + devVault = deployedContracts['devVault'] + devStrategy = deployedContracts['devStrategy'] + controller = deployedContracts['controller'] + sender = deployedContracts['sender'] + multisig = deployedContracts['multisig'] + property = devStrategy.property() + DEV_WHALE = '0xe23fe51187a807d56189212591f5525127003bdf' + + chain.sleep(5 * 86400) + chain.mine(1000) + + vaultDevBefore = dev.balanceOf(devVault.address) + controllerDevBefore = dev.balanceOf(controller.rewards()) + onlyStratDevB = dev.balanceOf(devStrategy.address) + strategyDevBefore = devStrategy.balanceOf() + + devStrategy.harvest(multisig) + + vaultDevAfter = dev.balanceOf(devVault.address) + controllerDevAfter = dev.balanceOf(controller.address) + onlyStratDevA = dev.balanceOf(devStrategy.address) + strategyDevAfter = devStrategy.balanceOf() + + assert strategyDevAfter > strategyDevBefore + assert onlyStratDevB == onlyStratDevA == 0 + assert controllerDevBefore == controllerDevAfter == 0 + assert vaultDevBefore == vaultDevAfter + + +def test_dev_vault_withdraw(deployedContracts): + deployer = deployedContracts['deployer'] + dev = deployedContracts['dev'] + devVault = deployedContracts['devVault'] + devStrategy = deployedContracts['devStrategy'] + sender = deployedContracts['sender'] + controller = deployedContracts['controller'] + property = devStrategy.property() + DEV_WHALE = '0xe23fe51187a807d56189212591f5525127003bdf' + + vaultDevBefore = dev.balanceOf(devVault.address) + strategyDevBefore = dev.balanceOf(devStrategy.address) + controllerDevBefore = dev.balanceOf(controller.address) + senderDevBefore = dev.balanceOf(sender['from']) + treasuryDevBefore = dev.balanceOf(controller.rewards()) + + devVault.withdraw(10000 * 10**18, sender) + + vaultDevAfter = dev.balanceOf(devVault.address) + strategyDevAfter = dev.balanceOf(devStrategy.address) + controllerDevAfter = dev.balanceOf(controller.address) + senderDevAfter = dev.balanceOf(sender['from']) + treasuryDevAfter = dev.balanceOf(controller.rewards()) + + assert treasuryDevAfter > treasuryDevBefore + assert senderDevAfter > senderDevBefore + assert controllerDevBefore == controllerDevAfter == 0 + assert strategyDevBefore == strategyDevAfter == 0 + + +def test_dev_vault_withdrawAll(deployedContracts): + deployer = deployedContracts['deployer'] + dev = deployedContracts['dev'] + devVault = deployedContracts['devVault'] + devStrategy = deployedContracts['devStrategy'] + sender = deployedContracts['sender'] + controller = deployedContracts['controller'] + property = devStrategy.property() + DEV_WHALE = '0xe23fe51187a807d56189212591f5525127003bdf' + + vaultDevBefore = dev.balanceOf(devVault.address) + strategyDevBefore = dev.balanceOf(devStrategy.address) + controllerDevBefore = dev.balanceOf(controller.address) + senderDevBefore = dev.balanceOf(sender['from']) + treasuryDevBefore = dev.balanceOf(controller.rewards()) + + devVault.withdrawAll(sender) + + vaultDevAfter = dev.balanceOf(devVault.address) + strategyDevAfter = dev.balanceOf(devStrategy.address) + controllerDevAfter = dev.balanceOf(controller.address) + senderDevAfter = dev.balanceOf(sender['from']) + treasuryDevAfter = dev.balanceOf(controller.rewards()) + + assert treasuryDevAfter > treasuryDevBefore + assert senderDevAfter > senderDevBefore + assert controllerDevBefore == controllerDevAfter == 0 + assert strategyDevBefore == strategyDevAfter == 0 + + +def test_dev_vault_deposit_withdraw_for_whale(deployedContracts): + deployer = deployedContracts['deployer'] + dev = deployedContracts['dev'] + devVault = deployedContracts['devVault'] + devStrategy = deployedContracts['devStrategy'] + sender = deployedContracts['sender'] + controller = deployedContracts['controller'] + multisig = deployedContracts['multisig'] + property = devStrategy.property() + DEV_WHALE = '0xe23fe51187a807d56189212591f5525127003bdf' + dev_whale = {'from': DEV_WHALE} + + strategyDevBefore = dev.balanceOf(devStrategy.address) + controllerDevBefore = dev.balanceOf(controller.address) + senderDevBefore = dev.balanceOf(DEV_WHALE) + treasuryDevBefore = dev.balanceOf(controller.rewards()) + + dev.approve(devVault.address, dev.balanceOf(DEV_WHALE), dev_whale) + devVault.depositAll(dev_whale) + devVault.earn(deployer) + chain.sleep(5 * 86400) + chain.mine(1000) + devStrategy.harvest(multisig) + devVault.withdraw(devVault.balanceOf(DEV_WHALE), dev_whale) + + strategyDevAfter = dev.balanceOf(devStrategy.address) + controllerDevAfter = dev.balanceOf(controller.address) + senderDevAfter = dev.balanceOf(DEV_WHALE) + treasuryDevAfter = dev.balanceOf(controller.rewards()) + + assert treasuryDevAfter > treasuryDevBefore + assert senderDevAfter < senderDevBefore + assert controllerDevBefore == controllerDevAfter == 0 + assert strategyDevBefore == strategyDevAfter == 0 + + +def test_dev_controller_withdrawAll(deployedContracts): + deployer = deployedContracts['deployer'] + dev = deployedContracts['dev'] + devVault = deployedContracts['devVault'] + devStrategy = deployedContracts['devStrategy'] + sender = deployedContracts['sender'] + controller = deployedContracts['controller'] + multisig = deployedContracts['multisig'] + property = devStrategy.property() + DEV_WHALE = '0xe23fe51187a807d56189212591f5525127003bdf' + + vaultDevBefore = dev.balanceOf(devVault.address) + strategyDevBefore = dev.balanceOf(devStrategy.address) + controllerDevBefore = dev.balanceOf(controller.address) + treasuryDevBefore = dev.balanceOf(controller.rewards()) + + controller.withdrawAll(dev.address, multisig) + + vaultDevAfter = dev.balanceOf(devVault.address) + strategyDevAfter = dev.balanceOf(devStrategy.address) + controllerDevAfter = dev.balanceOf(controller.address) + treasuryDevAfter = dev.balanceOf(controller.rewards()) + + assert treasuryDevAfter == treasuryDevBefore + assert vaultDevAfter > vaultDevBefore + assert controllerDevBefore == controllerDevAfter == 0 + assert strategyDevBefore == strategyDevAfter == 0 + + +def test_dev_strategy_withdrawToVault(deployedContracts): + deployer = deployedContracts['deployer'] + dev = deployedContracts['dev'] + devVault = deployedContracts['devVault'] + devStrategy = deployedContracts['devStrategy'] + sender = deployedContracts['sender'] + controller = deployedContracts['controller'] + multisig = deployedContracts['multisig'] + property = devStrategy.property() + DEV_WHALE = '0xe23fe51187a807d56189212591f5525127003bdf' + + # cannot test due to brownie's internal issue + # devStrategy.withdrawToVault(10000 * 10**18, multisig) diff --git a/protocol/tests/frax/test_frax.py b/protocol/tests/frax/test_frax.py new file mode 100644 index 0000000..bef6feb --- /dev/null +++ b/protocol/tests/frax/test_frax.py @@ -0,0 +1,91 @@ +import pytest +from brownie import * +import typer +import json + +# tests written in /sd-convex repo + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts[0] + + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + TREASURY_VAULT = deployed[ENV]['TREASURY_VAULT'] + treasuryVault = Contract(TREASURY_VAULT) + + SBTC_CRV = deployed[ENV]['SBTC_CRV'] + + THREE_CRV = deployed[ENV]['THREE_CRV'] + + EURS_CRV = deployed[ENV]['EURS_CRV'] + + DAI = deployed[ENV]['DAI'] + dai = Contract(DAI) + + VESTING_ESCROW = deployed[ENV]['VESTING_ESCROW'] + + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + multisig = {'from': GNOSIS_SAFE_PROXY} + + CONTROLLER = deployed[ENV]['CONTROLLER'] + controller = Controller.at(CONTROLLER) + + DEV = '0x5cAf454Ba92e6F2c929DF14667Ee360eD9fD5b26' + PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A' + + sender = {'from': '0x9f535E3c63Cd1447164BED4933d1efefBbC97a3f'} + + dev = Contract(DEV) + + vault = Vault.at('0x99780beAdd209cc3c7282536883Ef58f4ff4E52F') + strat = StrategyFrxConvex.deploy(CONTROLLER, PROXY, deployer) + WHALE = '0x07A75Ba044cDAaa624aAbAD27CB95C42510AF4B5' + whale = {'from': WHALE} + vault.deposit(100_000 * 10**18, whale) + + return {"dai": dai, "treasuryVault": treasuryVault, "multisig": multisig, "deployer": deployer, "DAI": DAI, "vault": vault, "SBTC_CRV": SBTC_CRV, "THREE_CRV": THREE_CRV, "EURS_CRV": EURS_CRV, "devVault": devVault, "devStrategy": devStrategy, "sender": sender, "controller": controller} + + +def test_frax_vault_deposit(deployedContracts): + dev = deployedContracts['dev'] + vault = deployedContracts['vault'] + devStrategy = deployedContracts['devStrategy'] + sender = deployedContracts['sender'] + + shareBefore = devVault.balanceOf(sender['from']) + senderDevBefore = dev.balanceOf(sender['from']) + vaultDevBefore = dev.balanceOf(devVault.address) + + dev.approve(devVault.address, dev.balanceOf(sender['from']), sender) + devVault.deposit(dev.balanceOf(sender['from']), sender) + + shareAfter = devVault.balanceOf(sender['from']) + senderDevAfter = dev.balanceOf(sender['from']) + vaultDevAfter = dev.balanceOf(devVault.address) + + print("Minted shares", (shareAfter - shareBefore) / 10**18) + + assert shareAfter - shareBefore > 0 + assert senderDevBefore - senderDevAfter > 0 + assert senderDevBefore - senderDevAfter == vaultDevAfter - vaultDevBefore + + # tests written in /sd-convex repo diff --git a/protocol/tests/functional/conftest.py b/protocol/tests/functional/conftest.py new file mode 100644 index 0000000..6d5ca0e --- /dev/null +++ b/protocol/tests/functional/conftest.py @@ -0,0 +1,37 @@ +import pytest + + +@pytest.fixture +def rewards(a): + yield a[2] + + +@pytest.fixture +def gov(a): + yield a[3] + + +@pytest.fixture +def token(a, Token): + # Must be ERC20 + yield a[0].deploy(Token) + + +@pytest.fixture +def controller(a): + yield a[4] + + +@pytest.fixture +def andre(accounts): + return accounts.at("0x2D407dDb06311396fE14D4b49da5F0471447d45C", force=True) + + +@pytest.fixture +def ychad(accounts): + return accounts.at("0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52", force=True) + + +@pytest.fixture +def binance(accounts): + return accounts.at("0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE", force=True) diff --git a/protocol/tests/functional/controllers/__init__.py b/protocol/tests/functional/controllers/__init__.py new file mode 100644 index 0000000..7b0a236 --- /dev/null +++ b/protocol/tests/functional/controllers/__init__.py @@ -0,0 +1 @@ +# Just here to disambiguate the test files (can't use same name without a module) diff --git a/protocol/tests/functional/controllers/test_config.py b/protocol/tests/functional/controllers/test_config.py new file mode 100644 index 0000000..14924eb --- /dev/null +++ b/protocol/tests/functional/controllers/test_config.py @@ -0,0 +1,38 @@ +import pytest +import brownie + + +def test_controller_deployment(gov, rewards, Controller): + controller = gov.deploy(Controller, rewards) + # Double check all the deployment variable values + assert controller.governance() == gov + assert controller.rewards() == rewards + assert controller.onesplit() == "0x50FDA034C0Ce7a8f7EFDAebDA7Aa7cA21CC1267e" + assert controller.split() == 500 + + +@pytest.mark.parametrize( + "getter,setter,val", + [ + ("split", "setSplit", 1000), + ("onesplit", "setOneSplit", None), + ("governance", "setGovernance", None), + ], +) +def test_controller_setParams(accounts, gov, rewards, getter, setter, val, Controller): + if not val: + # Can't access fixtures, so use None to mean an address literal + val = accounts[1] + + controller = gov.deploy(Controller, rewards) + + # Only governance can set this param + with brownie.reverts("!governance"): + getattr(controller, setter)(val, {"from": accounts[1]}) + getattr(controller, setter)(val, {"from": gov}) + assert getattr(controller, getter)() == val + + # When changing governance contract, make sure previous no longer has access + if getter == "governance": + with brownie.reverts("!governance"): + getattr(controller, setter)(val, {"from": gov}) diff --git a/protocol/tests/functional/strategies/__init__.py b/protocol/tests/functional/strategies/__init__.py new file mode 100644 index 0000000..7b0a236 --- /dev/null +++ b/protocol/tests/functional/strategies/__init__.py @@ -0,0 +1 @@ +# Just here to disambiguate the test files (can't use same name without a module) diff --git a/protocol/tests/functional/strategies/test_config.py b/protocol/tests/functional/strategies/test_config.py new file mode 100644 index 0000000..5d858e6 --- /dev/null +++ b/protocol/tests/functional/strategies/test_config.py @@ -0,0 +1,74 @@ +import pytest +import brownie + + +from brownie import ( + StrategyCreamYFI, + StrategyCurveBTCVoterProxy, + StrategyCurveBUSDVoterProxy, + StrategyCurveYVoterProxy, + StrategyCurve3CrvVoterProxy, + StrategyDAICurve, + StrategyDForceUSDC, + StrategyDForceUSDT, + StrategyMKRVaultDAIDelegate, + StrategyTUSDCurve, + StrategyVaultUSDC, +) + +STRATEGIES = [ + StrategyCreamYFI, + StrategyCurveBTCVoterProxy, + StrategyCurveBUSDVoterProxy, + StrategyCurveYVoterProxy, + StrategyCurve3CrvVoterProxy, + StrategyDAICurve, + StrategyDForceUSDC, + StrategyDForceUSDT, + StrategyMKRVaultDAIDelegate, + StrategyTUSDCurve, + StrategyVaultUSDC, +] + + +@pytest.mark.parametrize("Strategy", STRATEGIES) +def test_strategy_deployment(gov, controller, Strategy): + strategy = gov.deploy(Strategy, controller) + # Double check all the deployment variable values + assert strategy.governance() == gov + assert strategy.controller() == controller + assert strategy.getName() == Strategy._name + + +@pytest.mark.parametrize( + "getter,setter,val", + [ + ("governance", "setGovernance", None), + ("controller", "setController", None), + ("strategist", "setStrategist", None), + ("fee", "setFee", 100), + ("withdrawalFee", "setWithdrawalFee", 100), + ("performanceFee", "setPerformanceFee", 1000), + ], +) +@pytest.mark.parametrize("Strategy", STRATEGIES) +def test_strategy_setParams(accounts, gov, controller, getter, setter, val, Strategy): + if not val: + # Can't access fixtures, so use None to mean an address literal + val = accounts[1] + + strategy = gov.deploy(Strategy, controller) + + if not hasattr(strategy, getter): + return # Some combinations aren't valid + + # Only governance can set this param + with brownie.reverts(): + getattr(strategy, setter)(val, {"from": accounts[1]}) + getattr(strategy, setter)(val, {"from": gov}) + assert getattr(strategy, getter)() == val + + # When changing governance contract, make sure previous no longer has access + if getter == "governance": + with brownie.reverts("!governance"): + getattr(strategy, setter)(val, {"from": gov}) diff --git a/protocol/tests/functional/utils/test_oracles.py b/protocol/tests/functional/utils/test_oracles.py new file mode 100644 index 0000000..72ef9c9 --- /dev/null +++ b/protocol/tests/functional/utils/test_oracles.py @@ -0,0 +1,69 @@ +import pytest + +from brownie import ( + BTCOSMedianizer, + ETHOSMedianizer, +) + +ORACLES = [BTCOSMedianizer, ETHOSMedianizer] + + +@pytest.mark.parametrize( + "Oracle,osm,medianizer", + [ + ( + BTCOSMedianizer, + "0xf185d0682d50819263941e5f4EacC763CC5C6C42", + "0x9B8Eb8b3d6e2e0Db36F41455185FEF7049a35CaE", + ), + ( + ETHOSMedianizer, + "0x81FE72B5A8d1A857d176C3E7d5Bd2679A9B85763", + "0x729D19f657BD0614b4985Cf1D82531c67569197B", + ), + ], +) +def test_hardcoded_config(a, Oracle, osm, medianizer): + oracle = a[0].deploy(Oracle) + assert oracle.OSM() == osm + assert oracle.MEDIANIZER() == medianizer + + +@pytest.mark.parametrize("Oracle", ORACLES) +def test_governance(a, gov, Oracle): + oracle = a[0].deploy(Oracle) + assert oracle.governance() == a[0] + oracle.setGovernance(gov) + assert oracle.governance() == gov + + +@pytest.mark.parametrize("Oracle", ORACLES) +def test_whitelist(a, gov, Oracle): + oracle = a[0].deploy(Oracle) + assert not oracle.authorized(gov) + oracle.setAuthorized(gov) + assert oracle.authorized(gov) + oracle.revokeAuthorized(gov) + assert not oracle.authorized(gov) + + +@pytest.mark.parametrize("Oracle", ORACLES) +@pytest.mark.parametrize("func", ["read", "foresight"]) +def test_read(a, Oracle, func): + oracle = a[0].deploy(Oracle) + price, osm = getattr(oracle, func)() + assert price > 0 + assert not osm + + +@pytest.mark.xfail +@pytest.mark.parametrize("func", ["read", "foresight"]) +def test_read_bud(a, interface, OSMedianizer, func): + oracle = OSMedianizer.at("0x82c93333e4E295AA17a05B15092159597e823e8a") + osm = interface.OracleSecurityModule(oracle.OSM()) + assert osm.bud(oracle), "kiss first" + reader = a[0] # TODO: someone authorized + assert oracle.authorized(reader) + price, osm = getattr(oracle, func)({"from": reader}) + assert price > 0 + assert osm diff --git a/protocol/tests/functional/vaults/__init__.py b/protocol/tests/functional/vaults/__init__.py new file mode 100644 index 0000000..7b0a236 --- /dev/null +++ b/protocol/tests/functional/vaults/__init__.py @@ -0,0 +1 @@ +# Just here to disambiguate the test files (can't use same name without a module) diff --git a/protocol/tests/functional/vaults/test_config.py b/protocol/tests/functional/vaults/test_config.py new file mode 100644 index 0000000..86702f9 --- /dev/null +++ b/protocol/tests/functional/vaults/test_config.py @@ -0,0 +1,55 @@ +import pytest +import brownie + +from brownie import ( + yVault, + yWETH, + yDelegatedVault, +) + +VAULTS = [yVault, yWETH, yDelegatedVault] + + +@pytest.mark.parametrize("Vault", VAULTS) +def test_vault_deployment(gov, token, controller, Vault): + vault = gov.deploy(Vault, token, controller) + # Addresses + assert vault.governance() == gov + assert vault.controller() == controller + assert vault.token() == token + # UI Stuff + assert vault.name() == "yearn " + token.name() + assert vault.symbol() == "sd" + token.symbol() + assert vault.decimals() == token.decimals() + + +@pytest.mark.parametrize( + "getter,setter,val", + [ + ("min", "setMin", 9000), + ("healthFactor", "setHealthFactor", 100), + ("controller", "setController", None), + ("governance", "setGovernance", None), + ], +) +@pytest.mark.parametrize("Vault", VAULTS) +def test_vault_setParams(accounts, gov, token, controller, getter, setter, val, Vault): + if not val: + # Can't access fixtures, so use None to mean an address literal + val = accounts[1] + + vault = gov.deploy(Vault, token, controller) + + if not hasattr(vault, getter): + return # Some combinations aren't valid + + # Only governance can set this param + with brownie.reverts("!governance"): + getattr(vault, setter)(val, {"from": accounts[1]}) + getattr(vault, setter)(val, {"from": gov}) + assert getattr(vault, getter)() == val + + # When changing governance contract, make sure previous no longer has access + if getter == "governance": + with brownie.reverts("!governance"): + getattr(vault, setter)(val, {"from": gov}) diff --git a/protocol/tests/liquidations/test_dark_paradise.py b/protocol/tests/liquidations/test_dark_paradise.py new file mode 100644 index 0000000..fa74705 --- /dev/null +++ b/protocol/tests/liquidations/test_dark_paradise.py @@ -0,0 +1,262 @@ +import pytest +from brownie import * +import typer +import json +import brownie + +commonId = 223 +rareId = 332 +uniqueId = 333 + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts[0] + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + COMMON_USER = accounts[1] + commonUser = {'from': COMMON_USER} + + RARE_USER = accounts[2] + rareUser = {'from': RARE_USER} + + UNIQUE_USER = accounts[3] + uniqueUser = {'from': UNIQUE_USER} + + PLEB_USER = accounts[4] + plebUser = {'from': PLEB_USER} + + REWARDER = accounts[5] + rewarder = {'from': REWARDER} + + SDT = deployed[ENV]['SDT'] + sdt = Contract(SDT) + + WHALE = "0xc78fa2af0ca7990bb5ff32c9a728125be58cf247" + whale = {'from': WHALE} + sdt.transfer(REWARDER, sdt.balanceOf(WHALE)/5, whale) + sdt.transfer(COMMON_USER, sdt.balanceOf(WHALE)/4, whale) + sdt.transfer(RARE_USER, sdt.balanceOf(WHALE)/3, whale) + sdt.transfer(UNIQUE_USER, sdt.balanceOf(WHALE)/2, whale) + sdt.transfer(PLEB_USER, sdt.balanceOf(WHALE), whale) + + nft = StratAccessNFT.deploy(ZERO_ADDRESS, deployer) + + darkParadise = DarkParadise.deploy(SDT, nft.address, deployer) + darkParadise.setRewardDistribution(REWARDER) + nft.addStrategy(darkParadise.address, deployer) + + sdt.approve(darkParadise.address, sdt.balanceOf(REWARDER), rewarder) + sdt.approve(darkParadise.address, sdt.balanceOf(COMMON_USER), commonUser) + sdt.approve(darkParadise.address, sdt.balanceOf(RARE_USER), rareUser) + sdt.approve(darkParadise.address, sdt.balanceOf(UNIQUE_USER), uniqueUser) + + return { + 'DEFAULT_DEPLOYER_ACCOUNT': DEFAULT_DEPLOYER_ACCOUNT, + 'deployer': deployer, + 'nft': nft, + 'sdt': sdt, + 'darkParadise': darkParadise, + 'COMMON_USER': COMMON_USER, + 'commonUser': commonUser, + 'RARE_USER': RARE_USER, + 'rareUser': rareUser, + 'UNIQUE_USER': UNIQUE_USER, + 'uniqueUser': uniqueUser, + 'PLEB_USER': PLEB_USER, + 'plebUser': plebUser, + 'REWARDER': REWARDER, + 'rewarder': rewarder + } + +def test_create_nfts(deployedContracts): + nft = deployedContracts['nft'] + deployer = deployedContracts['deployer'] + DEFAULT_DEPLOYER_ACCOUNT = deployedContracts['DEFAULT_DEPLOYER_ACCOUNT'] + COMMON_USER = deployedContracts['COMMON_USER'] + RARE_USER = deployedContracts['RARE_USER'] + UNIQUE_USER = deployedContracts['UNIQUE_USER'] + + for i in range(1, 112): + tx = nft.create(1, 1, "", "", deployer) + id = tx.return_value + assert id == i + 222 # nft ids start from 223 for this version of nft + + nft.safeTransferFrom(DEFAULT_DEPLOYER_ACCOUNT, COMMON_USER, commonId, 1, "", deployer) + nft.safeTransferFrom(DEFAULT_DEPLOYER_ACCOUNT, RARE_USER, rareId, 1, "", deployer) + nft.safeTransferFrom(DEFAULT_DEPLOYER_ACCOUNT, UNIQUE_USER, uniqueId, 1, "", deployer) + +def test_enter_leave(deployedContracts): + sdt = deployedContracts['sdt'] + darkParadise = deployedContracts['darkParadise'] + COMMON_USER = deployedContracts['COMMON_USER'] + commonUser = deployedContracts['commonUser'] + + shareBefore = darkParadise.balanceOf(COMMON_USER) + senderSDTBefore = sdt.balanceOf(COMMON_USER) + paradiseSDTBefore = sdt.balanceOf(darkParadise.address) + + darkParadise.enter(4_000 * 10**18, commonId, commonUser) + + shareMiddle = darkParadise.balanceOf(COMMON_USER) + senderSDTMiddle = sdt.balanceOf(COMMON_USER) + paradiseSDTMiddle = sdt.balanceOf(darkParadise.address) + + assert shareMiddle - shareBefore == 4_000 * 10**18 + assert senderSDTBefore - senderSDTMiddle == 4_000 * 10**18 + assert paradiseSDTMiddle - paradiseSDTBefore == 4_000 * 10**18 + + darkParadise.leave(4_000 * 10**18, commonUser) + + shareAfter = darkParadise.balanceOf(COMMON_USER) + senderSDTAfter = sdt.balanceOf(COMMON_USER) + paradiseSDTAfter = sdt.balanceOf(darkParadise.address) + + assert shareAfter == 0 + assert senderSDTAfter - senderSDTMiddle == 4_000 * 10**18 + assert paradiseSDTMiddle - paradiseSDTAfter == 4_000 * 10**18 + +def test_common_limit(deployedContracts): + darkParadise = deployedContracts['darkParadise'] + commonUser = deployedContracts['commonUser'] + + darkParadise.enter(9_000 * 10**18, commonId, commonUser) + + with brownie.reverts('Limit hit'): + darkParadise.enter(2_000 * 10**18, commonId, commonUser) + + darkParadise.leave(9_000 * 10**18, commonUser) + +def test_rare_limit(deployedContracts): + darkParadise = deployedContracts['darkParadise'] + rareUser = deployedContracts['rareUser'] + + darkParadise.enter(29_000 * 10**18, rareId, rareUser) + + with brownie.reverts('Limit hit'): + darkParadise.enter(2_000 * 10**18, rareId, rareUser) + + darkParadise.leave(29_000 * 10**18, rareUser) + +def test_unique_limit(deployedContracts): + darkParadise = deployedContracts['darkParadise'] + uniqueUser = deployedContracts['uniqueUser'] + + darkParadise.enter(119_000 * 10**18, uniqueId, uniqueUser) + + with brownie.reverts('Limit hit'): + darkParadise.enter(2_000 * 10**18, uniqueId, uniqueUser) + + darkParadise.leave(119_000 * 10**18, uniqueUser) + +def test_transfer_restrictions(deployedContracts): + nft = deployedContracts['nft'] + darkParadise = deployedContracts['darkParadise'] + COMMON_USER = deployedContracts['COMMON_USER'] + commonUser = deployedContracts['commonUser'] + PLEB_USER = deployedContracts['PLEB_USER'] + plebUser = deployedContracts['plebUser'] + + darkParadise.enter(9_000 * 10**18, commonId, commonUser) + + with brownie.reverts('StratAccessNFT: NFT being used in strategy'): + nft.safeTransferFrom(COMMON_USER, PLEB_USER, commonId, 1, "", commonUser) + + with brownie.reverts('Restricted'): + darkParadise.transfer(PLEB_USER, 1_000 * 10**18, commonUser) + + with brownie.reverts('Restricted'): + darkParadise.transferFrom(COMMON_USER, PLEB_USER, 1_000 * 10**18, plebUser) + + darkParadise.leave(9_000 * 10**18, commonUser) + + nft.safeTransferFrom(COMMON_USER, PLEB_USER, commonId, 1, "", commonUser) + nft.safeTransferFrom(PLEB_USER, COMMON_USER, commonId, 1, "", plebUser) + +def test_multiple_enter_leave(deployedContracts): + darkParadise = deployedContracts['darkParadise'] + sdt = deployedContracts['sdt'] + commonUser = deployedContracts['commonUser'] + COMMON_USER = deployedContracts['COMMON_USER'] + rareUser = deployedContracts['rareUser'] + RARE_USER = deployedContracts['RARE_USER'] + uniqueUser = deployedContracts['uniqueUser'] + UNIQUE_USER = deployedContracts['UNIQUE_USER'] + rewarder = deployedContracts['rewarder'] + + startCommonBal = sdt.balanceOf(COMMON_USER) + startRareBal = sdt.balanceOf(RARE_USER) + startUniqueBal = sdt.balanceOf(UNIQUE_USER) + + darkParadise.enter(6_000, commonId, commonUser) + darkParadise.enter(16_000, rareId, rareUser) + darkParadise.enter(61_000, uniqueId, uniqueUser) + assert darkParadise.balanceOf(COMMON_USER) == 6_000 + assert darkParadise.balanceOf(RARE_USER) == 16_000 + assert darkParadise.balanceOf(UNIQUE_USER) == 61_000 + assert darkParadise.totalSupply() == 83_000 + assert sdt.balanceOf(darkParadise.address) == 83_000 + + darkParadise.notifyRewardAmount(10_000, rewarder) + assert sdt.balanceOf(darkParadise.address) == 93_000 + + darkParadise.enter(3_000, commonId, commonUser) + darkParadise.enter(12_000, rareId, rareUser) + darkParadise.enter(50_000, uniqueId, uniqueUser) + assert darkParadise.balanceOf(COMMON_USER) == 8_677 # 6000 + 3000 * (83000 / 93000) + assert darkParadise.balanceOf(RARE_USER) == 26_709 # 16000 + 12000 * (83000 / 93000) + assert darkParadise.balanceOf(UNIQUE_USER) == 105_623 # 61000 + 50000 * (83000 / 93000) + assert darkParadise.totalSupply() == 141_009 + assert sdt.balanceOf(darkParadise.address) == 158_000 + + darkParadise.notifyRewardAmount(22_000, rewarder) + assert sdt.balanceOf(darkParadise.address) == 180_000 + + darkParadise.leave(4_000, commonUser) + darkParadise.leave(11_000, rareUser) + darkParadise.leave(40_000, uniqueUser) + assert darkParadise.balanceOf(COMMON_USER) == 4_677 # 8677 - 4000 + assert darkParadise.balanceOf(RARE_USER) == 15_709 # 26709 - 11000 + assert darkParadise.balanceOf(UNIQUE_USER) == 65_623 # 105623 - 40000 + assert darkParadise.totalSupply() == 86_009 + assert sdt.balanceOf(COMMON_USER) == startCommonBal - 6000 - 3000 + 5106 # 4000 * 180000 / 141009 + assert sdt.balanceOf(RARE_USER) == startRareBal - 16000 - 12000 + 14041 # 11000 * 180000 / 141009 + assert sdt.balanceOf(UNIQUE_USER) == startUniqueBal - 61000 - 50000 + 51060 # 40000 * 180000 / 141009 + +def test_burn(deployedContracts): + darkParadise = deployedContracts['darkParadise'] + deployer = deployedContracts['deployer'] + nft = deployedContracts['nft'] + commonUser = deployedContracts['commonUser'] + COMMON_USER = deployedContracts['COMMON_USER'] + + darkParadise.enter(3_000, commonId, commonUser) + + oldNFTSupply = nft.totalSupply(commonId) + + with brownie.reverts('Ownable: caller is not the owner'): + nft.burn(COMMON_USER, commonId, 1, commonUser) + + with brownie.reverts('StratAccessNFT: NFT being used in strategy'): + nft.burn(COMMON_USER, commonId, 1, deployer) + + shares = darkParadise.balanceOf(COMMON_USER) + darkParadise.leave(shares, commonUser) + + nft.burn(COMMON_USER, commonId, 1, deployer) + + assert nft.totalSupply(commonId) == oldNFTSupply - 1 diff --git a/protocol/tests/liquidations/test_deathgod.py b/protocol/tests/liquidations/test_deathgod.py new file mode 100644 index 0000000..12f297a --- /dev/null +++ b/protocol/tests/liquidations/test_deathgod.py @@ -0,0 +1,155 @@ +import pytest +from brownie import * +import typer +import json +import brownie + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts[0] + + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + TREASURY_VAULT = deployed[ENV]['TREASURY_VAULT'] + treasuryVault = Contract(TREASURY_VAULT) + + SDT = deployed[ENV]['SDT'] + sdt = Contract(SDT) + + DAI = deployed[ENV]['DAI'] + dai = Contract(DAI) + + USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' + usdc = FiatTokenV2_1.at(USDC) + + USDC_WHALE = '0x55fe002aeff02f77364de339a1292923a15844b8' + usdc_whale = {'from': USDC_WHALE} + + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + DYDXERC3156 = "0x6bdC1FCB2F13d1bA9D26ccEc3983d5D4bf318693" + TIP_JAR = "0x5312B0d160E16feeeec13437a0053009e7564287" + + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + multisig = {'from': GNOSIS_SAFE_PROXY} + # random 2nd argument for testing + darkParadise = DarkParadise.deploy( + SDT, '0x87535b160E251167FB7abE239d2467d1127219E4', deployer) + + deathGod = DeathGod.deploy( + DEFAULT_DEPLOYER_ACCOUNT, darkParadise.address, DYDXERC3156, TIP_JAR, deployer) + + # set governance to multisig later + + usdc.transfer(deathGod.address, 3000 * 10**6, usdc_whale) + + return {"dai": dai, "treasuryVault": treasuryVault, "multisig": multisig, "deployer": deployer, "DAI": DAI, "SDT": SDT, "darkParadise": darkParadise, "deathGod": deathGod, "USDC_WHALE": USDC_WHALE, "usdc_whale": usdc_whale, "sdt": sdt, "usdc": usdc, "ZERO_ADDRESS": ZERO_ADDRESS} + + +def test_liquidateOnAave(deployedContracts): + deployer = deployedContracts["deployer"] + darkParadise = deployedContracts["darkParadise"] + deathGod = deployedContracts['deathGod'] + usdc = deployedContracts['usdc'] + sdt = deployedContracts['sdt'] + usdc_whale = deployedContracts['usdc_whale'] + ZERO_ADDRESS = deployedContracts['ZERO_ADDRESS'] + + WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + PLEB = "0x23f7c7d09f1d9fb6d182e910ccaaed37ddd87429" + DEBT_TO_COVER = "2533977845" + + zeroAddress = accounts.at(ZERO_ADDRESS, force=True) + # usdc.transfer(deathGod.address, 100_000 * 10**6, usdc_whale) + + ethB = zeroAddress.balance() + usdcBefore = usdc.balanceOf(deathGod.address) + deathGod.liquidateOnAave( + WETH, usdc.address, PLEB, DEBT_TO_COVER, False, 10, deployer) + + usdcAfter = usdc.balanceOf(deathGod.address) + ethA = zeroAddress.balance() + + print("DIFF", (usdcAfter - usdcBefore) / 10**6) + print("MINER_DIFF_ETH", (ethA - ethB) / 10**18) + + +# def test_sendSDTToDarkParadise(deployedContracts): +# deployer = deployedContracts["deployer"] +# darkParadise = deployedContracts["darkParadise"] +# deathGod = deployedContracts['deathGod'] +# usdc = deployedContracts['usdc'] +# sdt = deployedContracts['sdt'] +# usdc_whale = deployedContracts['usdc_whale'] +# ZERO_ADDRESS = deployedContracts['ZERO_ADDRESS'] + +# zeroAddress = accounts.at(ZERO_ADDRESS, force=True) +# usdc.transfer(deathGod.address, 100_000 * 10**6, usdc_whale) + +# eth0B = accounts[0].balance() +# ethB = zeroAddress.balance() +# sdtBefore = sdt.balanceOf(darkParadise.address) +# deathGod.sendSDTToDarkParadise( +# usdc.address, 100_000 * 10**6, {'from': accounts[0], 'value': 3 * 10**18}) +# sdtAfter = sdt.balanceOf(darkParadise.address) +# ethA = zeroAddress.balance() +# eth0A = accounts[0].balance() + +# print("DIFF", (sdtAfter - sdtBefore) / 10**18) +# print("DIFF_ETH", (ethA - ethB) / 10**18) +# print("DIFF_ETH_0", (eth0A - eth0B) / 10**18) + # print("CHAIN", chain[12470918]) + + +# def test_flashBorrow(deployedContracts): +# deployer = deployedContracts["deployer"] +# deathGod = deployedContracts['deathGod'] +# usdc = deployedContracts['usdc'] + +# usdcBefore = usdc.balanceOf(deathGod.address) +# deathGod.flashBorrow(usdc.address, 100_000 * 10**6, deployer) +# usdcAfter = usdc.balanceOf(deathGod.address) + +# print("DIFF", usdcBefore - usdcAfter) +# assert usdcBefore - usdcAfter == 2 + + +# def test_tipMinerInToken(deployedContracts): +# deployer = deployedContracts["deployer"] +# darkParadise = deployedContracts["darkParadise"] +# deathGod = deployedContracts['deathGod'] +# usdc = deployedContracts['usdc'] +# sdt = deployedContracts['sdt'] +# usdc_whale = deployedContracts['usdc_whale'] +# ZERO_ADDRESS = deployedContracts['ZERO_ADDRESS'] + +# zeroAddress = accounts.at(ZERO_ADDRESS, force=True) +# usdc.transfer(deathGod.address, 100_000 * 10**6, usdc_whale) + +# eth0B = accounts[0].balance() +# ethB = zeroAddress.balance() +# deathGod.tipMinerInToken( +# usdc.address, 5000 * 10**6, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", {'from': accounts[0]}) +# ethA = zeroAddress.balance() +# eth0A = accounts[0].balance() + +# print("DIFF_ETH", (ethA - ethB) / 10**18) +# print("DIFF_ETH_0", (eth0A - eth0B) / 10**18) + # print("CHAIN", chain[12470918]) diff --git a/protocol/tests/migration/__init__.py b/protocol/tests/migration/__init__.py new file mode 100644 index 0000000..7b0a236 --- /dev/null +++ b/protocol/tests/migration/__init__.py @@ -0,0 +1 @@ +# Just here to disambiguate the test files (can't use same name without a module) diff --git a/protocol/tests/migration/crv-migration.py b/protocol/tests/migration/crv-migration.py new file mode 100644 index 0000000..eb3dc6f --- /dev/null +++ b/protocol/tests/migration/crv-migration.py @@ -0,0 +1,1111 @@ +import pytest +from brownie import * +import typer +import json + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts.load('stakedao-deployer-rug-pull') + + print("DEFAULT_DEPLOYER_ACCOUNT: ", DEFAULT_DEPLOYER_ACCOUNT) + + # Deployed GnosisSafeProxy (mainnet) + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + print("GNOSIS_SAFE_PROXY: ", GNOSIS_SAFE_PROXY) + + # CRV Token mainnet address + CRV = deployed[ENV]['CRV'] + + print("CRV: ", CRV) + + # sbtcCrv Token mainnet address + SBTC_CRV = deployed[ENV]['SBTC_CRV'] + + print("SBTC_CRV: ", SBTC_CRV) + + # 3Crv Token mainnet address + THREE_CRV = deployed[ENV]['THREE_CRV'] + + print("THREE_CRV: ", THREE_CRV) + + # eursCrv Token mainnet address + EURS_CRV = deployed[ENV]['EURS_CRV'] + + print("EURS_CRV: ", EURS_CRV) + + # Strategist account + STRATEGIST = deployed[ENV]['STRATEGIST'] + + print("STRATEGIST: ", STRATEGIST) + + # TreasuryVault mainnet address + CONTROLLER = deployed[ENV]['CONTROLLER'] + + print("CONTROLLER: ", CONTROLLER) + + # CurveYCRVVoter mainnet address + CURVE_YCRV_VOTER = deployed[ENV]['CURVE_YCRV_VOTER'] + + print("CURVE_YCRV_VOTER: ", CURVE_YCRV_VOTER) + + # VE_CURVE_VAULT + VE_CURVE_VAULT = deployed[ENV]['VE_CURVE_VAULT'] + + print("VE_CURVE_VAULT: ", VE_CURVE_VAULT) + + # TREASURY_VAULT + TREASURY_VAULT = deployed[ENV]['TREASURY_VAULT'] + + print("TREASURY_VAULT: ", TREASURY_VAULT) + + # CRV mainnet address + CRV = deployed[ENV]['CRV'] + + print("CRV: ", CRV) + + SNX = "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F" + + print("SNX: ", SNX) + + EURS = "0xdB25f211AB05b1c97D595516F45794528a807ad8" + + print("EURS: ", EURS) + + # STRATEGY_CURVE_3CRV_VOTER_PROXY + STRATEGY_CURVE_3CRV_VOTER_PROXY = deployed[ENV]['STRATEGY_CURVE_3CRV_VOTER_PROXY'] + + print("STRATEGY_CURVE_3CRV_VOTER_PROXY: ", STRATEGY_CURVE_3CRV_VOTER_PROXY) + + # STRATEGY_CURVE_BTC_VOTER_PROXY + STRATEGY_CURVE_BTC_VOTER_PROXY = deployed[ENV]['STRATEGY_CURVE_BTC_VOTER_PROXY'] + + print("STRATEGY_CURVE_BTC_VOTER_PROXY: ", STRATEGY_CURVE_BTC_VOTER_PROXY) + + # CONTROLLER + CONTROLLER = deployed[ENV]['CONTROLLER'] + + print("CONTROLLER: ", CONTROLLER) + + # EURS_CRV_VAULT + EURS_CRV_VAULT = deployed[ENV]['EURS_CRV_VAULT'] + + print("EURS_CRV_VAULT: ", EURS_CRV_VAULT) + + # THREE_POOL_VAULT + THREE_POOL_VAULT = "0xB17640796e4c27a39AF51887aff3F8DC0daF9567" + + print("THREE_POOL_VAULT: ", THREE_POOL_VAULT) + + # SBTC_VAULT + SBTC_VAULT = "0x24129B935AfF071c4f0554882C0D9573F4975fEd" + + print("SBTC_VAULT: ", SBTC_VAULT) + + OLD_STRATEGY_CURVE_EURS_CRV_VOTER_PROXY = "0xa69cfa7F5e7Bcd9b4862D2660d8C9D336D8Ab900" + + OLD_CURVE_YCRV_VOTER = "0x96032427893A22dd2a8FDb0e5fE09abEfc9E4444" + + VOTING_ESCROW = "0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2" + + # Old StrategyCurve3CrvVoterProxy + oldStrategyCurveEursCrvVoterProxy = StrategyCurveEursCrvVoterProxy.at( + OLD_STRATEGY_CURVE_EURS_CRV_VOTER_PROXY) + + print("Old StrategyCurveEursCrvVoterProxy: ", + oldStrategyCurveEursCrvVoterProxy.address) + + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + multisig = {'from': GNOSIS_SAFE_PROXY} + + controller = Controller.at(CONTROLLER) + + treasuryVault = TreasuryVault.at(TREASURY_VAULT) + + # Deploy StrategyCurveEursCrvVoterProxy + strategyCurveEursCrvVoterProxy = StrategyCurveEursCrvVoterProxy.deploy( + controller.address, deployer) + + # Deploy StrategyProxy + strategyProxy = StrategyProxy.deploy(deployer) + + # Deploy WhitehatStrategyProxy + whitehatStrategyProxy = WhitehatStrategyProxy.deploy(deployer) + + strategyCurveBTCVoterProxy = StrategyCurveBTCVoterProxy.at( + STRATEGY_CURVE_BTC_VOTER_PROXY) + + strategyCurve3CrvVoterProxy = StrategyCurve3CrvVoterProxy.at( + STRATEGY_CURVE_3CRV_VOTER_PROXY) + + veCurve = veCurveVault.at(VE_CURVE_VAULT) + + eursCrvVault = yVault.at(EURS_CRV_VAULT) + + threePoolVault = yVault.at(THREE_POOL_VAULT) + + sbtcCrvVault = yVault.at(SBTC_VAULT) + + oldCurveYCRVVoter = CurveYCRVVoter.at(OLD_CURVE_YCRV_VOTER) + + curveYCRVVoter = CurveYCRVVoter.at(CURVE_YCRV_VOTER) + + eursCrv = VaultToken.at(EURS_CRV) + + threeCrv = VaultToken.at(THREE_CRV) + + sbtcCrv = VaultToken.at(SBTC_CRV) + + snx = Contract(SNX) + + eurs = Contract(EURS) + + crv = Contract(CRV) + + votingEscrow = Contract(VOTING_ESCROW) + + return {"treasuryVault": treasuryVault, "controller": controller, "votingEscrow": votingEscrow, "snx": snx, "eurs": eurs, "whitehatStrategyProxy": whitehatStrategyProxy, "strategyProxy": strategyProxy, "strategyCurveEursCrvVoterProxy": strategyCurveEursCrvVoterProxy, "deployer": deployer, "STRATEGIST": STRATEGIST, "GNOSIS_SAFE_PROXY": GNOSIS_SAFE_PROXY, "STRATEGY_CURVE_BTC_VOTER_PROXY": STRATEGY_CURVE_BTC_VOTER_PROXY, "GNOSIS_SAFE_PROXY": GNOSIS_SAFE_PROXY, "strategyCurveBTCVoterProxy": strategyCurveBTCVoterProxy, "strategyProxy": strategyProxy, "veCurve": veCurve, "controller": controller, "strategyCurveEursCrvVoterProxy": strategyCurveEursCrvVoterProxy, "eursCrvVault": eursCrvVault, "oldStrategyCurveEursCrvVoterProxy": oldStrategyCurveEursCrvVoterProxy, "oldCurveYCRVVoter": oldCurveYCRVVoter, "eursCrv": eursCrv, "multisig": multisig, "EURS_CRV": EURS_CRV, "strategyCurve3CrvVoterProxy": strategyCurve3CrvVoterProxy, "threeCrv": threeCrv, "sbtcCrv": sbtcCrv, "threePoolVault": threePoolVault, "sbtcCrvVault": sbtcCrvVault, "crv": crv, "curveYCRVVoter": curveYCRVVoter} + + +def test_strategyCurveEursCrvVoterProxy_approveStrategy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # Register strategyCurveEursCrvVoterProxy in StrategyProxy + strategyProxy.approveStrategy( + strategyCurveEursCrvVoterProxy.address, deployer) + + assert strategyProxy.strategies( + strategyCurveEursCrvVoterProxy.address) == True + + +def test_strategyCurveEursCrvVoterProxy_setProxy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # set proxy to StrategyProxy + strategyCurveEursCrvVoterProxy.setProxy( + strategyProxy.address, deployer) + + assert strategyCurveEursCrvVoterProxy.proxy() == strategyProxy.address + + +def test_strategyCurveEursCrvVoterProxy_setStrategist(deployedContracts): + STRATEGIST = deployedContracts["STRATEGIST"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # Transfer strategyCurveEursCrvVoterProxy strategist role to Strategist + strategyCurveEursCrvVoterProxy.setStrategist( + STRATEGIST, deployer) + + assert strategyCurveEursCrvVoterProxy.strategist() == STRATEGIST + + +def test_strategyCurveEursCrvVoterProxy_setGovernance(deployedContracts): + GNOSIS_SAFE_PROXY = deployedContracts["GNOSIS_SAFE_PROXY"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # Transfer strategyCurveEursCrvVoterProxy governance to Governance (GSP) + strategyCurveEursCrvVoterProxy.setGovernance( + GNOSIS_SAFE_PROXY, deployer) + + assert strategyCurveEursCrvVoterProxy.governance() == GNOSIS_SAFE_PROXY + + +def test_strategyCurveBTCVoterProxy_approveStrategy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurveBTCVoterProxy = deployedContracts["strategyCurveBTCVoterProxy"] + deployer = deployedContracts["deployer"] + + # Register strategyCurveBTCVoterProxy in StrategyProxy + strategyProxy.approveStrategy(strategyCurveBTCVoterProxy.address, deployer) + + assert strategyProxy.strategies( + strategyCurveBTCVoterProxy.address) == True + + +def test_strategyCurve3CrvVoterProxy_approveStrategy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurve3CrvVoterProxy = deployedContracts["strategyCurve3CrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # Register strategyCurve3CrvVoterProxy in StrategyProxy + strategyProxy.approveStrategy( + strategyCurve3CrvVoterProxy.address, deployer) + + assert strategyProxy.strategies( + strategyCurve3CrvVoterProxy.address) == True + + +def test_strategyProxy_setGovernance(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + GNOSIS_SAFE_PROXY = deployedContracts["GNOSIS_SAFE_PROXY"] + deployer = deployedContracts["deployer"] + + # Transfer StrategyProxy governance to Governance (GSP) + strategyProxy.setGovernance(GNOSIS_SAFE_PROXY, deployer) + + assert strategyProxy.governance() == GNOSIS_SAFE_PROXY + + +def test_veCurve_setFeeDistribution(deployedContracts): + veCurve = deployedContracts["veCurve"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # Set Curve FeeDistribution contract to new strategyProxy + veCurve.setFeeDistribution(strategyProxy.address, multisig) + + assert veCurve.feeDistribution() == strategyProxy.address + + +def test_veCurve_setProxy(deployedContracts): + veCurve = deployedContracts["veCurve"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # Set new StrategyProxy + veCurve.setProxy(strategyProxy.address, multisig) + + assert veCurve.proxy() == strategyProxy.address + + +def test_strategyCurve3CrvVoterProxy_setProxy(deployedContracts): + strategyCurve3CrvVoterProxy = deployedContracts["strategyCurve3CrvVoterProxy"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # set proxy to new StrategyProxy + strategyCurve3CrvVoterProxy.setProxy(strategyProxy.address, multisig) + + assert strategyCurve3CrvVoterProxy.proxy() == strategyProxy.address + + +def test_strategyCurveBTCVoterProxy_setProxy(deployedContracts): + strategyCurveBTCVoterProxy = deployedContracts["strategyCurveBTCVoterProxy"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # set proxy to new StrategyProxy + strategyCurveBTCVoterProxy.setProxy(strategyProxy.address, multisig) + + assert strategyCurveBTCVoterProxy.proxy() == strategyProxy.address + + +def test_controller_approveStrategy(deployedContracts): + controller = deployedContracts["controller"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + multisig = deployedContracts["multisig"] + EURS_CRV = deployedContracts["EURS_CRV"] + + # Approve the strategyCurveEursCrvVoterProxy strategy in Controller + controller.approveStrategy( + EURS_CRV, strategyCurveEursCrvVoterProxy.address, multisig) + + assert controller.approvedStrategies( + EURS_CRV, strategyCurveEursCrvVoterProxy.address) == True + + +def test_controller_setStrategy(deployedContracts): + controller = deployedContracts["controller"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + oldStrategyCurveEursCrvVoterProxy = deployedContracts["oldStrategyCurveEursCrvVoterProxy"] + multisig = deployedContracts["multisig"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + + totalBalBefore = eursCrvVault.balance() + vaultBalBefore = eursCrvBal(deployedContracts, eursCrvVault.address) + oldStratBalBefore = oldStrategyCurveEursCrvVoterProxy.balanceOf() + + # Register strategyCurveEursCrvVoterProxy in Controller + controller.setStrategy( + EURS_CRV, strategyCurveEursCrvVoterProxy.address, multisig) + + totalBalAfter = eursCrvVault.balance() + vaultBalAfter = eursCrv.balanceOf(eursCrvVault.address) + oldStratBalAfter = oldStrategyCurveEursCrvVoterProxy.balanceOf() + + assert totalBalBefore == totalBalAfter + assert vaultBalAfter == vaultBalBefore + oldStratBalBefore + assert vaultBalAfter == totalBalBefore + assert oldStratBalAfter == 0 + + +def test_controller_revokeStrategy(deployedContracts): + controller = deployedContracts["controller"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + oldStrategyCurveEursCrvVoterProxy = deployedContracts["oldStrategyCurveEursCrvVoterProxy"] + multisig = deployedContracts["multisig"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + + # Revoke old strategyCurveEursCrvVoterProxy in Controller + controller.revokeStrategy( + EURS_CRV, oldStrategyCurveEursCrvVoterProxy.address, multisig) + + assert oldStrategyCurveEursCrvVoterProxy.balanceOf() == 0 + assert controller.approvedStrategies( + EURS_CRV, oldStrategyCurveEursCrvVoterProxy.address) == False + + +def test_curveYCRVVoter_setStrategy(deployedContracts): + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # Set CurveYCRVVoter strategy to new StrategyProxy + curveYCRVVoter.setStrategy(strategyProxy.address, multisig) + + assert curveYCRVVoter.strategy() == strategyProxy.address + + +def test_strategyProxy_ifTokenGetStuckInProxy(deployedContracts): + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + strategyProxy = deployedContracts["strategyProxy"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + snx = deployedContracts["snx"] + multisig = deployedContracts["multisig"] + + strategyBalBefore = snxBal( + deployedContracts, strategyCurveEursCrvVoterProxy) + curveYCRVVoterBalBefore = snxBal( + deployedContracts, curveYCRVVoter) + + assert strategyBalBefore == 0 + assert curveYCRVVoterBalBefore > 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + strategyProxy.ifTokenGetStuckInProxy( + snx.address, strategyCurveEursCrvVoterProxy.address, multisig) + + strategyBalAfter = snxBal( + deployedContracts, strategyCurveEursCrvVoterProxy) + curveYCRVVoterBalAfter = snxBal( + deployedContracts, curveYCRVVoter) + + assert curveYCRVVoterBalAfter == 0 + assert curveYCRVVoterBalBefore == strategyBalAfter + assert snxBal(deployedContracts, strategyProxy) == 0 + + log("SNX Rescued: "+str(strategyBalAfter/10**18)) + + +def test_eursCrvVault_earn(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + controller = deployedContracts["controller"] + + totalBalBefore = eursCrvVault.balance() + vaultBalBefore = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + + assert stratBalBefore == 0 + assert totalBalBefore == vaultBalBefore + + # Call earn on eursCrv Vault to transfer the funds to the new strategy + eursCrvVault.earn(deployer) + + totalBalAfter = eursCrvVault.balance() + vaultBalAfter = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + + assert totalBalBefore == totalBalAfter + assert stratBalAfter > 0 + assert vaultBalBefore > vaultBalAfter + assert vaultBalBefore == vaultBalAfter + stratBalAfter + + assert crvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursCrvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveEursCrvVoterProxy) > 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert eursCrvBal(deployedContracts, strategyProxy) == 0 + assert eursBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_strategyCurveEursCrvVoterProxy_harvest(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + multisig = deployedContracts["multisig"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + votingEscrow = deployedContracts["votingEscrow"] + controller = deployedContracts["controller"] + treasuryVault = deployedContracts["treasuryVault"] + + chain.mine(1000) + + totalBalBefore = eursCrvVault.balance() + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + earnedBefore = strategyCurveEursCrvVoterProxy.earned() + lockedBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalBefore = eursCrvBal( + deployedContracts, treasuryVault.address) + + assert earnedBefore == 0 + + strategyCurveEursCrvVoterProxy.harvest(multisig) + + totalBalAfter = eursCrvVault.balance() + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + earnedAfter = strategyCurveEursCrvVoterProxy.earned() + lockedAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalAfter = eursCrvBal( + deployedContracts, treasuryVault.address) + + log("Total eursCrv Earned in 1000 blocks: " + + str((earnedAfter-earnedBefore)/10**18)) + log("Change in total eursCrv in Vault+Strategy+Gauge: " + + str((totalBalAfter-totalBalBefore)/10**18)) + log("Change in total eursCrv in Strategy+Gauge: " + + str((stratBalAfter-stratBalBefore)/10**18)) + + assert earnedAfter > 0 + assert totalBalAfter > totalBalBefore + assert stratBalAfter > stratBalBefore + assert lockedAfter > lockedBefore + assert treasuryVaultBalAfter > treasuryVaultBalBefore + + assert crvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursCrvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert eursCrvBal(deployedContracts, strategyProxy) == 0 + assert eursBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_strategyCurve3CrvVoterProxy_harvest(deployedContracts): + strategyCurve3CrvVoterProxy = deployedContracts["strategyCurve3CrvVoterProxy"] + deployer = deployedContracts["deployer"] + threePoolVault = deployedContracts["threePoolVault"] + multisig = deployedContracts["multisig"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + votingEscrow = deployedContracts["votingEscrow"] + controller = deployedContracts["controller"] + treasuryVault = deployedContracts["treasuryVault"] + + chain.mine(1000) + + totalBalBefore = threePoolVault.balance() + stratBalBefore = strategyCurve3CrvVoterProxy.balanceOf() + earnedBefore = strategyCurve3CrvVoterProxy.earned() + lockedBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalBefore = threeCrvBal( + deployedContracts, treasuryVault.address) + + strategyCurve3CrvVoterProxy.harvest(multisig) + + totalBalAfter = threePoolVault.balance() + stratBalAfter = strategyCurve3CrvVoterProxy.balanceOf() + earnedAfter = strategyCurve3CrvVoterProxy.earned() + lockedAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalAfter = threeCrvBal( + deployedContracts, treasuryVault.address) + + log("Total 3Crv Earned in 1000 blocks: " + + str((earnedAfter-earnedBefore)/10**18)) + log("Change in total 3Crv in Vault+Strategy+Gauge: " + + str((totalBalAfter-totalBalBefore)/10**18)) + log("Change in total 3Crv in Strategy+Gauge: " + + str((stratBalAfter-stratBalBefore)/10**18)) + + assert earnedAfter > 0 + assert totalBalAfter > totalBalBefore + assert stratBalAfter > stratBalBefore + assert treasuryVaultBalAfter > treasuryVaultBalBefore + + assert crvBal(deployedContracts, strategyCurve3CrvVoterProxy) == 0 + assert threeCrvBal(deployedContracts, strategyCurve3CrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurve3CrvVoterProxy) == 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert threeCrvBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert threeCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + assert lockedAfter > lockedBefore + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_strategyCurveBTCVoterProxy_harvest(deployedContracts): + strategyCurveBTCVoterProxy = deployedContracts["strategyCurveBTCVoterProxy"] + deployer = deployedContracts["deployer"] + sbtcCrvVault = deployedContracts["sbtcCrvVault"] + multisig = deployedContracts["multisig"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + votingEscrow = deployedContracts["votingEscrow"] + controller = deployedContracts["controller"] + treasuryVault = deployedContracts["treasuryVault"] + + chain.mine(1000) + + totalBalBefore = sbtcCrvVault.balance() + stratBalBefore = strategyCurveBTCVoterProxy.balanceOf() + earnedBefore = strategyCurveBTCVoterProxy.earned() + lockedBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalBefore = sbtcCrvBal( + deployedContracts, treasuryVault.address) + + strategyCurveBTCVoterProxy.harvest(multisig) + + totalBalAfter = sbtcCrvVault.balance() + stratBalAfter = strategyCurveBTCVoterProxy.balanceOf() + earnedAfter = strategyCurveBTCVoterProxy.earned() + lockedAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalAfter = sbtcCrvBal( + deployedContracts, treasuryVault.address) + + log("Total sbtcCrv Earned in 1000 blocks: " + + str((earnedAfter-earnedBefore)/10**18)) + log("Change in total sbtcCrv in Vault+Strategy+Gauge: " + + str((totalBalAfter-totalBalBefore)/10**18)) + log("Change in total sbtcCrv in Strategy+Gauge: " + + str((stratBalAfter-stratBalBefore)/10**18)) + + assert earnedAfter > 0 + assert totalBalAfter > totalBalBefore + assert stratBalAfter > stratBalBefore + assert treasuryVaultBalAfter > treasuryVaultBalBefore + + assert crvBal(deployedContracts, strategyCurveBTCVoterProxy) == 0 + assert sbtcCrvBal(deployedContracts, strategyCurveBTCVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveBTCVoterProxy) == 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert sbtcCrvBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert sbtcCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + assert lockedAfter > lockedBefore + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_eursCrvVault_deposit(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + controller = deployedContracts["controller"] + + totalBalBefore = eursCrvVault.balance() + vaultBalBefore = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + + DEPOSIT_AMOUNT = 50 * (10**6) * (10**18) + + eursCrv.transfer(deployer["from"].address, DEPOSIT_AMOUNT, { + "from": "0xc0d8994Cd78eE1980885DF1A0C5470fC977b5cFe"}) + + eursCrv.approve(eursCrvVault.address, DEPOSIT_AMOUNT, deployer) + + # Call deposit on eursCrv Vault + eursCrvVault.deposit(DEPOSIT_AMOUNT, deployer) + + totalBalAfter = eursCrvVault.balance() + vaultBalAfter = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + + assert eursCrvVault.balanceOf(deployer["from"].address) > 0 + + assert stratBalAfter == stratBalBefore + assert totalBalAfter == totalBalBefore + DEPOSIT_AMOUNT + assert vaultBalAfter == vaultBalBefore + DEPOSIT_AMOUNT + + assert crvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursCrvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert eursCrvBal(deployedContracts, strategyProxy) == 0 + assert eursBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_eursCrvVault_earn_2(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + controller = deployedContracts["controller"] + + chain.mine(1000) + + totalBalBefore = eursCrvVault.balance() + vaultBalBefore = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + + # Call earn on eursCrv Vault to transfer the funds to the new strategy + eursCrvVault.earn(deployer) + + totalBalAfter = eursCrvVault.balance() + vaultBalAfter = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + + assert totalBalBefore == totalBalAfter + assert stratBalAfter - stratBalBefore == vaultBalBefore - vaultBalAfter + + assert crvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursCrvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveEursCrvVoterProxy) > 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert eursCrvBal(deployedContracts, strategyProxy) == 0 + assert eursBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_eursCrvVault_withdraw(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + controller = deployedContracts["controller"] + + chain.mine(1000) + + totalBalBefore = eursCrvVault.balance() + vaultBalBefore = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + + TOTAL_BAL = eursCrvVault.balanceOf(deployer["from"].address) + + WITHDRAW_AMOUNT = TOTAL_BAL*(4/5) + + # Call withdraw on eursCrv Vault + eursCrvVault.withdraw(WITHDRAW_AMOUNT, deployer) + + totalBalAfter = eursCrvVault.balance() + vaultBalAfter = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + + assert eursCrvVault.balanceOf(deployer["from"].address) > 0 + + assert stratBalAfter < stratBalBefore + assert totalBalAfter < totalBalBefore + assert vaultBalAfter < vaultBalBefore + assert vaultBalAfter == 0 + + assert crvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursCrvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveEursCrvVoterProxy) > 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert eursCrvBal(deployedContracts, strategyProxy) == 0 + assert eursBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_strategyCurveEursCrvVoterProxy_withdrawToVault(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + multisig = deployedContracts["multisig"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + controller = deployedContracts["controller"] + + chain.mine(1000) + + totalBalBefore = eursCrvVault.balance() + vaultBalBefore = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + + WITHDRAW_AMOUNT = stratBalBefore/10 + + # Call withdraw on eursCrv Vault + strategyCurveEursCrvVoterProxy.withdrawToVault(WITHDRAW_AMOUNT, multisig) + + totalBalAfter = eursCrvVault.balance() + vaultBalAfter = eursCrvBal(deployedContracts, eursCrvVault.address) + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + + assert totalBalAfter == totalBalBefore + assert vaultBalAfter == vaultBalBefore + WITHDRAW_AMOUNT + assert stratBalAfter == stratBalBefore - WITHDRAW_AMOUNT + + assert crvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursCrvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveEursCrvVoterProxy) > 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert eursCrvBal(deployedContracts, strategyProxy) == 0 + assert eursBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_strategyCurveEursCrvVoterProxy_harvest_2(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + multisig = deployedContracts["multisig"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + votingEscrow = deployedContracts["votingEscrow"] + controller = deployedContracts["controller"] + treasuryVault = deployedContracts["treasuryVault"] + + chain.mine(1000) + + totalBalBefore = eursCrvVault.balance() + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + earnedBefore = strategyCurveEursCrvVoterProxy.earned() + lockedBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalBefore = eursCrvBal( + deployedContracts, treasuryVault.address) + + strategyCurveEursCrvVoterProxy.harvest(multisig) + + totalBalAfter = eursCrvVault.balance() + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + earnedAfter = strategyCurveEursCrvVoterProxy.earned() + lockedAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + treasuryVaultBalAfter = eursCrvBal( + deployedContracts, treasuryVault.address) + + log("Total eursCrv Earned in 1000 blocks: " + + str((earnedAfter-earnedBefore)/10**18)) + log("Change in total eursCrv in Vault+Strategy+Gauge: " + + str((totalBalAfter-totalBalBefore)/10**18)) + log("Change in total eursCrv in Strategy+Gauge: " + + str((stratBalAfter-stratBalBefore)/10**18)) + + assert earnedAfter > earnedBefore + assert totalBalAfter > totalBalBefore + assert stratBalAfter > stratBalBefore + assert treasuryVaultBalAfter > treasuryVaultBalBefore + + assert crvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursCrvBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert eursBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + assert snxBal(deployedContracts, strategyCurveEursCrvVoterProxy) == 0 + + assert crvBal(deployedContracts, strategyProxy) == 0 + assert eursCrvBal(deployedContracts, strategyProxy) == 0 + assert eursBal(deployedContracts, strategyProxy) == 0 + assert snxBal(deployedContracts, strategyProxy) == 0 + + assert crvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursCrvBal(deployedContracts, curveYCRVVoter) == 0 + assert eursBal(deployedContracts, curveYCRVVoter) == 0 + assert snxBal(deployedContracts, curveYCRVVoter) == 0 + assert lockedAfter > lockedBefore + + assert crvBal(deployedContracts, controller) == 0 + assert eursCrvBal(deployedContracts, controller) == 0 + assert eursBal(deployedContracts, controller) == 0 + assert snxBal(deployedContracts, controller) == 0 + + +def test_strategyProxy_ifTokenGetStuck(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + multisig = deployedContracts["multisig"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + votingEscrow = deployedContracts["votingEscrow"] + + # Send some assets to StrategyProxy + crv = Contract("0xD533a949740bb3306d119CC777fa900bA034cd52") + snx = Contract("0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F") + eursCrv = Contract("0x194eBd173F6cDacE046C53eACcE9B953F28411d1") + threeCrv = Contract("0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490") + + AMOUNT = 10 ** 18 + + crv.transfer(strategyProxy.address, AMOUNT, + {"from": "0xD533a949740bb3306d119CC777fa900bA034cd52"}) + snx.transfer(strategyProxy.address, AMOUNT, + {"from": "0xDA4eF8520b1A57D7d63f1E249606D1A459698876"}) + eursCrv.transfer(strategyProxy.address, AMOUNT, + {"from": "0xc0d8994Cd78eE1980885DF1A0C5470fC977b5cFe"}) + threeCrv.transfer(strategyProxy.address, AMOUNT, + {"from": "0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490"}) + + assert crv.balanceOf(strategyProxy.address) >= AMOUNT + assert snx.balanceOf(strategyProxy.address) >= AMOUNT + assert eursCrv.balanceOf(strategyProxy.address) >= AMOUNT + assert threeCrv.balanceOf(strategyProxy.address) >= AMOUNT + + beforeCrvBal = crv.balanceOf(multisig["from"]) + beforeSnxBal = snx.balanceOf(multisig["from"]) + beforeEursCrvBal = eursCrv.balanceOf(multisig["from"]) + beforeThreeCrvBal = threeCrv.balanceOf(multisig["from"]) + + # Get assets out from the StrategyProxy + strategyProxy.ifTokenGetStuck(crv.address, multisig["from"], multisig) + strategyProxy.ifTokenGetStuck(snx.address, multisig["from"], multisig) + strategyProxy.ifTokenGetStuck(eursCrv.address, multisig["from"], multisig) + strategyProxy.ifTokenGetStuck(threeCrv.address, multisig["from"], multisig) + + afterCrvBal = crv.balanceOf(multisig["from"]) + afterSnxBal = snx.balanceOf(multisig["from"]) + afterEursCrvBal = eursCrv.balanceOf(multisig["from"]) + afterThreeCrvBal = threeCrv.balanceOf(multisig["from"]) + + assert crv.balanceOf(strategyProxy.address) == 0 + assert snx.balanceOf(strategyProxy.address) == 0 + assert eursCrv.balanceOf(strategyProxy.address) == 0 + assert threeCrv.balanceOf(strategyProxy.address) == 0 + + assert afterCrvBal - beforeCrvBal >= AMOUNT + assert afterSnxBal - beforeSnxBal >= AMOUNT + assert afterEursCrvBal - beforeEursCrvBal >= AMOUNT + assert afterThreeCrvBal - beforeThreeCrvBal >= AMOUNT + + +def test_strategyProxy_ifTokenGetStuckInProxy_2(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + multisig = deployedContracts["multisig"] + strategyProxy = deployedContracts["strategyProxy"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + votingEscrow = deployedContracts["votingEscrow"] + + # Send some assets to StrategyProxy + crv = Contract("0xD533a949740bb3306d119CC777fa900bA034cd52") + snx = Contract("0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F") + eursCrv = Contract("0x194eBd173F6cDacE046C53eACcE9B953F28411d1") + threeCrv = Contract("0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490") + + AMOUNT = 10 ** 18 + + crv.transfer(curveYCRVVoter.address, AMOUNT, + {"from": "0xD533a949740bb3306d119CC777fa900bA034cd52"}) + snx.transfer(curveYCRVVoter.address, AMOUNT, + {"from": "0xDA4eF8520b1A57D7d63f1E249606D1A459698876"}) + eursCrv.transfer(curveYCRVVoter.address, AMOUNT, + {"from": "0xc0d8994Cd78eE1980885DF1A0C5470fC977b5cFe"}) + threeCrv.transfer(curveYCRVVoter.address, AMOUNT, + {"from": "0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490"}) + + assert crv.balanceOf(curveYCRVVoter.address) >= AMOUNT + assert snx.balanceOf(curveYCRVVoter.address) >= AMOUNT + assert eursCrv.balanceOf(curveYCRVVoter.address) >= AMOUNT + assert threeCrv.balanceOf(curveYCRVVoter.address) >= AMOUNT + + beforeCrvBal = crv.balanceOf(multisig["from"]) + beforeSnxBal = snx.balanceOf(multisig["from"]) + beforeEursCrvBal = eursCrv.balanceOf(multisig["from"]) + beforeThreeCrvBal = threeCrv.balanceOf(multisig["from"]) + + # Get assets out from the StrategyProxy + strategyProxy.ifTokenGetStuckInProxy( + crv.address, multisig["from"], multisig) + strategyProxy.ifTokenGetStuckInProxy( + snx.address, multisig["from"], multisig) + strategyProxy.ifTokenGetStuckInProxy( + eursCrv.address, multisig["from"], multisig) + strategyProxy.ifTokenGetStuckInProxy( + threeCrv.address, multisig["from"], multisig) + + afterCrvBal = crv.balanceOf(multisig["from"]) + afterSnxBal = snx.balanceOf(multisig["from"]) + afterEursCrvBal = eursCrv.balanceOf(multisig["from"]) + afterThreeCrvBal = threeCrv.balanceOf(multisig["from"]) + + assert crv.balanceOf(curveYCRVVoter.address) == 0 + assert snx.balanceOf(curveYCRVVoter.address) == 0 + assert eursCrv.balanceOf(curveYCRVVoter.address) == 0 + assert threeCrv.balanceOf(curveYCRVVoter.address) == 0 + + assert afterCrvBal - beforeCrvBal >= AMOUNT + assert afterSnxBal - beforeSnxBal >= AMOUNT + assert afterEursCrvBal - beforeEursCrvBal >= AMOUNT + assert afterThreeCrvBal - beforeThreeCrvBal >= AMOUNT + + +def test_oldCurveYCrvVoter_setStrategy(deployedContracts): + oldCurveYCRVVoter = deployedContracts["oldCurveYCRVVoter"] + whitehatStrategyProxy = deployedContracts["whitehatStrategyProxy"] + multisig = deployedContracts["multisig"] + + # set whitehatStrategyProxy as strategy for oldCurveYCrvVoter + oldCurveYCRVVoter.setStrategy(whitehatStrategyProxy.address, multisig) + + assert oldCurveYCRVVoter.strategy() == whitehatStrategyProxy.address + + +def test_whitehatStrategyProxy_rescueCrv(deployedContracts): + oldCurveYCRVVoter = deployedContracts["oldCurveYCRVVoter"] + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + strategyProxy = deployedContracts["strategyProxy"] + whitehatStrategyProxy = deployedContracts["whitehatStrategyProxy"] + deployer = deployedContracts["deployer"] + votingEscrow = deployedContracts["votingEscrow"] + + oldCurveYCRVVoterBalBefore = crvBal(deployedContracts, oldCurveYCRVVoter) + lockedBefore = votingEscrow.balanceOf(curveYCRVVoter.address) + + assert oldCurveYCRVVoterBalBefore > 0 + + # Withdraw and transfer CRV from oldCurveYCrvVoter to the new strategyCurveEursCrvVoterProxy + whitehatStrategyProxy.rescueCrv( + curveYCRVVoter.address, strategyProxy.address, deployer) + + oldCurveYCRVVoterBalAfter = crvBal(deployedContracts, oldCurveYCRVVoter) + lockedAfter = votingEscrow.balanceOf(curveYCRVVoter.address) + + log("CRV Rescued: "+str((lockedAfter-lockedBefore)/10**18)) + + assert oldCurveYCRVVoterBalAfter == 0 + assert lockedAfter - lockedBefore > 0 + + +def crvBal(deployedContracts, address): + crv = deployedContracts["crv"] + return crv.balanceOf(address) + + +def eursCrvBal(deployedContracts, address): + eursCrv = deployedContracts["eursCrv"] + return eursCrv.balanceOf(address) + + +def eursBal(deployedContracts, address): + eurs = deployedContracts["eurs"] + return eurs.balanceOf(address) + + +def threeCrvBal(deployedContracts, address): + threeCrv = deployedContracts["threeCrv"] + return threeCrv.balanceOf(address) + + +def sbtcCrvBal(deployedContracts, address): + sbtcCrvBal = deployedContracts["sbtcCrv"] + return sbtcCrvBal.balanceOf(address) + + +def snxBal(deployedContracts, address): + snx = deployedContracts["snx"] + return snx.balanceOf(address) + + +def log(msg): + msg = typer.style(msg, fg=typer.colors.GREEN, bold=True) + typer.echo(msg) diff --git a/protocol/tests/migration/migrate.py b/protocol/tests/migration/migrate.py new file mode 100644 index 0000000..b65a530 --- /dev/null +++ b/protocol/tests/migration/migrate.py @@ -0,0 +1,504 @@ +import pytest +from brownie import * +import typer +import json + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts.load('stakedao-deployer-rug-pull') + + print("DEFAULT_DEPLOYER_ACCOUNT: ", DEFAULT_DEPLOYER_ACCOUNT) + + # Deployed GnosisSafeProxy (mainnet) + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + print("GNOSIS_SAFE_PROXY: ", GNOSIS_SAFE_PROXY) + + # CRV Token mainnet address + CRV = deployed[ENV]['CRV'] + + print("CRV: ", CRV) + + # sbtcCrv Token mainnet address + SBTC_CRV = deployed[ENV]['SBTC_CRV'] + + print("SBTC_CRV: ", SBTC_CRV) + + # 3Crv Token mainnet address + THREE_CRV = deployed[ENV]['THREE_CRV'] + + print("THREE_CRV: ", THREE_CRV) + + # eursCrv Token mainnet address + EURS_CRV = deployed[ENV]['EURS_CRV'] + + print("EURS_CRV: ", EURS_CRV) + + # TODO: Strategist account + STRATEGIST = deployed[ENV]['STRATEGIST'] + + print("STRATEGIST: ", STRATEGIST) + + # TreasuryVault mainnet address + CONTROLLER = deployed[ENV]['CONTROLLER'] + + print("CONTROLLER: ", CONTROLLER) + + # CurveYCRVVoter mainnet address + CURVE_YCRV_VOTER = deployed[ENV]['CURVE_YCRV_VOTER'] + + print("CURVE_YCRV_VOTER: ", CURVE_YCRV_VOTER) + + # VE_CURVE_VAULT + VE_CURVE_VAULT = deployed[ENV]['VE_CURVE_VAULT'] + + print("VE_CURVE_VAULT: ", VE_CURVE_VAULT) + + # CRV mainnet address + CRV = deployed[ENV]['CRV'] + + print("CRV: ", CRV) + + # STRATEGY_CURVE_3CRV_VOTER_PROXY + STRATEGY_CURVE_3CRV_VOTER_PROXY = deployed[ENV]['STRATEGY_CURVE_3CRV_VOTER_PROXY'] + + print("STRATEGY_CURVE_3CRV_VOTER_PROXY: ", STRATEGY_CURVE_3CRV_VOTER_PROXY) + + # STRATEGY_CURVE_BTC_VOTER_PROXY + STRATEGY_CURVE_BTC_VOTER_PROXY = deployed[ENV]['STRATEGY_CURVE_BTC_VOTER_PROXY'] + + print("STRATEGY_CURVE_BTC_VOTER_PROXY: ", STRATEGY_CURVE_BTC_VOTER_PROXY) + + # EURS_CRV_VAULT + EURS_CRV_VAULT = deployed[ENV]['EURS_CRV_VAULT'] + + print("EURS_CRV_VAULT: ", EURS_CRV_VAULT) + + # THREE_POOL_VAULT + THREE_POOL_VAULT = "0xB17640796e4c27a39AF51887aff3F8DC0daF9567" + + print("THREE_POOL_VAULT: ", THREE_POOL_VAULT) + + # SBTC_VAULT + SBTC_VAULT = "0x24129B935AfF071c4f0554882C0D9573F4975fEd" + + print("SBTC_VAULT: ", SBTC_VAULT) + + CURVE_DAO = "0x40907540d8a6C65c637785e8f8B742ae6b0b9968" + + SMART_WALLET_WHITELIST = "0xca719728Ef172d0961768581fdF35CB116e0B7a4" + + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + multisig = {'from': GNOSIS_SAFE_PROXY} + + # Deploy StrategyProxy + strategyProxy = StrategyProxy.deploy(deployer) + + controller = Controller.at(CONTROLLER) + + # Deploy StrategyCurveEursCrvVoterProxy + strategyCurveEursCrvVoterProxy = StrategyCurveEursCrvVoterProxy.deploy( + controller.address, deployer) + + strategyCurveBTCVoterProxy = StrategyCurveBTCVoterProxy.at( + STRATEGY_CURVE_BTC_VOTER_PROXY) + + strategyCurve3CrvVoterProxy = StrategyCurve3CrvVoterProxy.at( + STRATEGY_CURVE_3CRV_VOTER_PROXY) + + oldStrategyCurveEursCrvVoterProxy = StrategyCurveEursCrvVoterProxy.at( + "0xc8a753B38978aDD5bD26A5D1290Abc6f9f2c4f99") + + veCurve = veCurveVault.at(VE_CURVE_VAULT) + + eursCrvVault = yVault.at(EURS_CRV_VAULT) + + threePoolVault = yVault.at(THREE_POOL_VAULT) + + sbtcCrvVault = yVault.at(SBTC_VAULT) + + curveYCRVVoter = CurveYCRVVoter.at(CURVE_YCRV_VOTER) + + eursCrv = VaultToken.at(EURS_CRV) + + threeCrv = VaultToken.at(THREE_CRV) + + sbtcCrv = VaultToken.at(SBTC_CRV) + + crv = Contract(CRV) + + return {"strategyProxy": strategyProxy, "strategyCurveEursCrvVoterProxy": strategyCurveEursCrvVoterProxy, "deployer": deployer, "STRATEGIST": STRATEGIST, "GNOSIS_SAFE_PROXY": GNOSIS_SAFE_PROXY, "STRATEGY_CURVE_BTC_VOTER_PROXY": STRATEGY_CURVE_BTC_VOTER_PROXY, "GNOSIS_SAFE_PROXY": GNOSIS_SAFE_PROXY, "strategyCurveBTCVoterProxy": strategyCurveBTCVoterProxy, "strategyProxy": strategyProxy, "veCurve": veCurve, "controller": controller, "strategyCurveEursCrvVoterProxy": strategyCurveEursCrvVoterProxy, "eursCrvVault": eursCrvVault, "oldStrategyCurveEursCrvVoterProxy": oldStrategyCurveEursCrvVoterProxy, "curveYCRVVoter": curveYCRVVoter, "eursCrv": eursCrv, "multisig": multisig, "EURS_CRV": EURS_CRV, "strategyCurve3CrvVoterProxy": strategyCurve3CrvVoterProxy, "CURVE_DAO": CURVE_DAO, "SMART_WALLET_WHITELIST": SMART_WALLET_WHITELIST, "threeCrv": threeCrv, "sbtcCrv": sbtcCrv, "threePoolVault": threePoolVault, "sbtcCrvVault": sbtcCrvVault, "crv": crv} + + +def test_strategyCurveEursCrvVoterProxy_approveStrategy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # Register strategyCurveEursCrvVoterProxy in StrategyProxy + strategyProxy.approveStrategy( + strategyCurveEursCrvVoterProxy.address, deployer) + + assert strategyProxy.strategies( + strategyCurveEursCrvVoterProxy.address) == True + + +def test_strategyCurveEursCrvVoterProxy_setProxy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # set proxy to StrategyProxy + strategyCurveEursCrvVoterProxy.setProxy( + strategyProxy.address, deployer) + + assert strategyCurveEursCrvVoterProxy.proxy() == strategyProxy.address + + +def test_strategyCurveEursCrvVoterProxy_setStrategist(deployedContracts): + STRATEGIST = deployedContracts["STRATEGIST"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # Transfer strategyCurveEursCrvVoterProxy strategist role to Strategist + strategyCurveEursCrvVoterProxy.setStrategist( + STRATEGIST, deployer) + + assert strategyCurveEursCrvVoterProxy.strategist() == STRATEGIST + + +def test_strategyCurveEursCrvVoterProxy_setGovernance(deployedContracts): + GNOSIS_SAFE_PROXY = deployedContracts["GNOSIS_SAFE_PROXY"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # Transfer strategyCurveEursCrvVoterProxy governance to Governance (GSP) + strategyCurveEursCrvVoterProxy.setGovernance( + GNOSIS_SAFE_PROXY, deployer) + + assert strategyCurveEursCrvVoterProxy.governance() == GNOSIS_SAFE_PROXY + + +def test_strategyCurveBTCVoterProxy_approveStrategy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurveBTCVoterProxy = deployedContracts["strategyCurveBTCVoterProxy"] + deployer = deployedContracts["deployer"] + + # Register strategyCurveBTCVoterProxy in StrategyProxy + strategyProxy.approveStrategy(strategyCurveBTCVoterProxy.address, deployer) + + assert strategyProxy.strategies( + strategyCurveBTCVoterProxy.address) == True + + +def test_strategyCurve3CrvVoterProxy_approveStrategy(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + strategyCurve3CrvVoterProxy = deployedContracts["strategyCurve3CrvVoterProxy"] + deployer = deployedContracts["deployer"] + + # Register strategyCurve3CrvVoterProxy in StrategyProxy + strategyProxy.approveStrategy( + strategyCurve3CrvVoterProxy.address, deployer) + + assert strategyProxy.strategies( + strategyCurve3CrvVoterProxy.address) == True + + +def test_strategyProxy_setGovernance(deployedContracts): + strategyProxy = deployedContracts["strategyProxy"] + GNOSIS_SAFE_PROXY = deployedContracts["GNOSIS_SAFE_PROXY"] + deployer = deployedContracts["deployer"] + + # Transfer StrategyProxy governance to Governance (GSP) + strategyProxy.setGovernance(GNOSIS_SAFE_PROXY, deployer) + + assert strategyProxy.governance() == GNOSIS_SAFE_PROXY + + +def test_veCurve_acceptGovernance(deployedContracts): + veCurve = deployedContracts["veCurve"] + GNOSIS_SAFE_PROXY = deployedContracts["GNOSIS_SAFE_PROXY"] + multisig = deployedContracts["multisig"] + + # multisig accept Governance of veCurve vault + veCurve.acceptGovernance(multisig) + + assert veCurve.governance() == GNOSIS_SAFE_PROXY + + +def test_veCurve_setFeeDistribution(deployedContracts): + veCurve = deployedContracts["veCurve"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # Set Curve FeeDistribution contract to new strategyProxy + veCurve.setFeeDistribution(strategyProxy.address, multisig) + + assert veCurve.feeDistribution() == strategyProxy.address + + +def test_veCurve_setProxy(deployedContracts): + veCurve = deployedContracts["veCurve"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # Set new StrategyProxy + veCurve.setProxy(strategyProxy.address, multisig) + + assert veCurve.proxy() == strategyProxy.address + + +def test_strategyCurve3CrvVoterProxy_setProxy(deployedContracts): + strategyCurve3CrvVoterProxy = deployedContracts["strategyCurve3CrvVoterProxy"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # set proxy to new StrategyProxy + strategyCurve3CrvVoterProxy.setProxy(strategyProxy.address, multisig) + + assert strategyCurve3CrvVoterProxy.proxy() == strategyProxy.address + + +def test_strategyCurveBTCVoterProxy_setProxy(deployedContracts): + strategyCurveBTCVoterProxy = deployedContracts["strategyCurveBTCVoterProxy"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # set proxy to new StrategyProxy + strategyCurveBTCVoterProxy.setProxy(strategyProxy.address, multisig) + + assert strategyCurveBTCVoterProxy.proxy() == strategyProxy.address + + +def test_controller_approveStrategy(deployedContracts): + controller = deployedContracts["controller"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + multisig = deployedContracts["multisig"] + EURS_CRV = deployedContracts["EURS_CRV"] + + # Approve the strategyCurveEursCrvVoterProxy strategy in Controller + controller.approveStrategy( + EURS_CRV, strategyCurveEursCrvVoterProxy.address, multisig) + + assert controller.approvedStrategies( + EURS_CRV, strategyCurveEursCrvVoterProxy.address) == True + + +def test_controller_setStrategy(deployedContracts): + controller = deployedContracts["controller"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + oldStrategyCurveEursCrvVoterProxy = deployedContracts["oldStrategyCurveEursCrvVoterProxy"] + multisig = deployedContracts["multisig"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + + totalBalBefore = eursCrvVault.balance() + vaultBalBefore = eursCrv.balanceOf(eursCrvVault.address) + oldStratBalBefore = oldStrategyCurveEursCrvVoterProxy.balanceOf() + + # Register strategyCurveEursCrvVoterProxy in Controller + controller.setStrategy( + EURS_CRV, strategyCurveEursCrvVoterProxy.address, multisig) + + totalBalAfter = eursCrvVault.balance() + vaultBalAfter = eursCrv.balanceOf(eursCrvVault.address) + oldStratBalAfter = oldStrategyCurveEursCrvVoterProxy.balanceOf() + + assert totalBalBefore == totalBalAfter + assert vaultBalAfter == vaultBalBefore + oldStratBalBefore + assert vaultBalAfter == totalBalBefore + assert oldStratBalAfter == 0 + + +def test_controller_revokeStrategy(deployedContracts): + controller = deployedContracts["controller"] + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + oldStrategyCurveEursCrvVoterProxy = deployedContracts["oldStrategyCurveEursCrvVoterProxy"] + multisig = deployedContracts["multisig"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + + # Revoke old strategyCurveEursCrvVoterProxy in Controller + controller.revokeStrategy( + EURS_CRV, oldStrategyCurveEursCrvVoterProxy.address, multisig) + + assert oldStrategyCurveEursCrvVoterProxy.balanceOf() == 0 + assert controller.approvedStrategies( + EURS_CRV, oldStrategyCurveEursCrvVoterProxy.address) == False + + +def test_curveYCRVVoter_setStrategy(deployedContracts): + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + strategyProxy = deployedContracts["strategyProxy"] + multisig = deployedContracts["multisig"] + + # Set CurveYCRVVoter strategy to new StrategyProxy + curveYCRVVoter.setStrategy(strategyProxy.address, multisig) + + assert curveYCRVVoter.strategy() == strategyProxy.address + + +def test_eursCrvVault_earn(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + EURS_CRV = deployedContracts["EURS_CRV"] + eursCrv = deployedContracts["eursCrv"] + + totalBalBefore = eursCrvVault.balance() + vaultBalBefore = eursCrv.balanceOf(eursCrvVault.address) + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + + assert stratBalBefore == 0 + assert totalBalBefore == vaultBalBefore + + # Call earn on eursCrv Vault to transfer the funds to the new strategy + eursCrvVault.earn(deployer) + + totalBalAfter = eursCrvVault.balance() + vaultBalAfter = eursCrv.balanceOf(eursCrvVault.address) + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + + assert totalBalBefore == totalBalAfter + assert stratBalAfter > 0 + assert vaultBalBefore > vaultBalAfter + assert vaultBalBefore == vaultBalAfter + stratBalAfter + + +def test_strategyCurveEursCrvVoterProxy_harvest(deployedContracts): + strategyCurveEursCrvVoterProxy = deployedContracts["strategyCurveEursCrvVoterProxy"] + deployer = deployedContracts["deployer"] + eursCrvVault = deployedContracts["eursCrvVault"] + multisig = deployedContracts["multisig"] + + totalBalBefore = eursCrvVault.balance() + stratBalBefore = strategyCurveEursCrvVoterProxy.balanceOf() + earnedBefore = strategyCurveEursCrvVoterProxy.earned() + + assert earnedBefore == 0 + + chain.mine(1000) + + strategyCurveEursCrvVoterProxy.harvest(multisig) + + totalBalAfter = eursCrvVault.balance() + stratBalAfter = strategyCurveEursCrvVoterProxy.balanceOf() + earnedAfter = strategyCurveEursCrvVoterProxy.earned() + + log("Total eursCrv Earned in 1000 blocks: " + str(earnedAfter)) + log("Change in total eursCrv in Vault+Strategy+Gauge: " + + str(totalBalAfter-totalBalBefore)) + log("Change in total eursCrv in Strategy+Gauge: " + + str(stratBalAfter-stratBalBefore)) + + assert earnedAfter > 0 + assert totalBalAfter > totalBalBefore + assert stratBalAfter > stratBalBefore + + +def test_whitelist(deployedContracts): + curveYCRVVoter = deployedContracts["curveYCRVVoter"] + crv = deployedContracts["crv"] + SMART_WALLET_WHITELIST = deployedContracts["SMART_WALLET_WHITELIST"] + CURVE_DAO = deployedContracts["CURVE_DAO"] + multisig = deployedContracts["multisig"] + + # Whitelist CurveYCRVVoter + smartWalletWhitelist = Contract(SMART_WALLET_WHITELIST) + smartWalletWhitelist.approveWallet( + curveYCRVVoter.address, {'from': CURVE_DAO}) + + # Send some Crv to CurveYCRVVoter (if no CRV in CurveYCRVVoter) + crv.transfer(curveYCRVVoter.address, 10**18, + {'from': '0xe3997288987E6297Ad550A69B31439504F513267'}) + + FOUR_YEARS_IN_SEC = 4 * 365 * 86400 + + # Create a CRV lock + curveYCRVVoter.createLock(10**18, chain.time()+FOUR_YEARS_IN_SEC, multisig) + + +def test_strategyCurve3CrvVoterProxy_harvest(deployedContracts): + strategyCurve3CrvVoterProxy = deployedContracts["strategyCurve3CrvVoterProxy"] + deployer = deployedContracts["deployer"] + threePoolVault = deployedContracts["threePoolVault"] + multisig = deployedContracts["multisig"] + + totalBalBefore = threePoolVault.balance() + stratBalBefore = strategyCurve3CrvVoterProxy.balanceOf() + earnedBefore = strategyCurve3CrvVoterProxy.earned() + + assert earnedBefore == 0 + + chain.mine(1000) + + strategyCurve3CrvVoterProxy.harvest(multisig) + + totalBalAfter = threePoolVault.balance() + stratBalAfter = strategyCurve3CrvVoterProxy.balanceOf() + earnedAfter = strategyCurve3CrvVoterProxy.earned() + + log("Total 3Crv Earned in 1000 blocks: " + str(earnedAfter)) + log("Change in total 3Crv in Vault+Strategy+Gauge: " + + str(totalBalAfter-totalBalBefore)) + log("Change in total 3Crv in Strategy+Gauge: " + + str(stratBalAfter-stratBalBefore)) + + assert earnedAfter > 0 + assert totalBalAfter > totalBalBefore + assert stratBalAfter > stratBalBefore + + +def test_strategyCurveBTCVoterProxy_harvest(deployedContracts): + strategyCurveBTCVoterProxy = deployedContracts["strategyCurveBTCVoterProxy"] + deployer = deployedContracts["deployer"] + sbtcCrvVault = deployedContracts["sbtcCrvVault"] + multisig = deployedContracts["multisig"] + + totalBalBefore = sbtcCrvVault.balance() + stratBalBefore = strategyCurveBTCVoterProxy.balanceOf() + earnedBefore = strategyCurveBTCVoterProxy.earned() + + assert earnedBefore == 0 + + chain.mine(1000) + + strategyCurveBTCVoterProxy.harvest(multisig) + + totalBalAfter = sbtcCrvVault.balance() + stratBalAfter = strategyCurveBTCVoterProxy.balanceOf() + earnedAfter = strategyCurveBTCVoterProxy.earned() + + log("Total sbtcCrv Earned in 1000 blocks: " + str(earnedAfter)) + log("Change in total sbtcCrv in Vault+Strategy+Gauge: " + + str(totalBalAfter-totalBalBefore)) + log("Change in total sbtcCrv in Strategy+Gauge: " + + str(stratBalAfter-stratBalBefore)) + + assert earnedAfter > 0 + assert totalBalAfter > totalBalBefore + assert stratBalAfter > stratBalBefore + + +def log(msg): + msg = typer.style(msg, fg=typer.colors.GREEN, bold=True) + typer.echo(msg) diff --git a/protocol/tests/multisig/timelock/__init__.py b/protocol/tests/multisig/timelock/__init__.py new file mode 100644 index 0000000..7b0a236 --- /dev/null +++ b/protocol/tests/multisig/timelock/__init__.py @@ -0,0 +1 @@ +# Just here to disambiguate the test files (can't use same name without a module) diff --git a/protocol/tests/multisig/timelock/test_masterchef.py b/protocol/tests/multisig/timelock/test_masterchef.py new file mode 100644 index 0000000..5580e62 --- /dev/null +++ b/protocol/tests/multisig/timelock/test_masterchef.py @@ -0,0 +1,46 @@ +import pytest +from brownie import * +import typer +import json + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts.load('stakedao-deployer-rug-pull') + + print("DEFAULT_DEPLOYER_ACCOUNT: ", DEFAULT_DEPLOYER_ACCOUNT) + + # Deployed GnosisSafeProxy (mainnet) + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + print("GNOSIS_SAFE_PROXY: ", GNOSIS_SAFE_PROXY) + +def test_queue_add_pools_to_masterchef(deployedContracts): + pass + + +def test_queue_set_pool_alloc_point(deployedContracts): + pass + + +def test_exec_set_pool_alloc_point(deployedContracts): + pass + + +def test_exec_set_pool_alloc_point(deployedContracts): + pass diff --git a/protocol/tests/nft/test_nft.py b/protocol/tests/nft/test_nft.py new file mode 100644 index 0000000..ccf12ed --- /dev/null +++ b/protocol/tests/nft/test_nft.py @@ -0,0 +1,374 @@ +import pytest +from brownie import * +import typer +import json +import brownie + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts[0] + + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + TREASURY_VAULT = deployed[ENV]['TREASURY_VAULT'] + treasuryVault = Contract(TREASURY_VAULT) + + SDT = deployed[ENV]['SDT'] + sdt = Contract(SDT) + + xsdt = Contract(deployed[ENV]['SANCTUARY']) + + SBTC_CRV = deployed[ENV]['SBTC_CRV'] + + THREE_CRV = deployed[ENV]['THREE_CRV'] + + EURS_CRV = deployed[ENV]['EURS_CRV'] + + DAI = deployed[ENV]['DAI'] + dai = Contract(DAI) + + VESTING_ESCROW = deployed[ENV]['VESTING_ESCROW'] + + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + SDT_STAKE_AMOUNT = 100*10**18 + + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + multisig = {'from': GNOSIS_SAFE_PROXY} + + # deploy NFT contract + nft = StakeDaoNFT.deploy(ZERO_ADDRESS, deployer) + + # deploy StakeDaoNFTPalace + nftPalace = StakeDaoNFTPalace.deploy(nft.address, xsdt.address, deployer) + + # give StakeDaoNFTPalace a minter role + nft.addMinter(nftPalace.address, deployer) + + XSDT_WHALE = '0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf' + xsdt_whale = {'from': XSDT_WHALE} + + xsdt.transfer(accounts[1], 2 * 10**22, xsdt_whale) + xsdt.transfer(accounts[2], 2 * 10**22, xsdt_whale) + xsdt.transfer(accounts[3], 3 * 10**22, xsdt_whale) + xsdt.transfer(accounts[4], 8 * 10**22, xsdt_whale) + + return {"dai": dai, "treasuryVault": treasuryVault, "multisig": multisig, "deployer": deployer, "DAI": DAI, "SDT": SDT, "SBTC_CRV": SBTC_CRV, "THREE_CRV": THREE_CRV, "EURS_CRV": EURS_CRV, "SDT_STAKE_AMOUNT": SDT_STAKE_AMOUNT, "sdt": sdt, "xsdt": xsdt, "nft": nft, "nftPalace": nftPalace, "VESTING_ESCROW": VESTING_ESCROW} + + +def test_create_id(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + + for i in range(1, 112): + tx = nft.create(1, 0, "", "", deployer) + id = tx.return_value + assert id == i + + +def test_add_card_cost(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + + for i in range(1, 112): + if i <= 100: + nftPalace.addCard(i, 400 * 10**18, deployer) + assert nftPalace.cards(i) == 400 * 10**18 + elif i <= 110: + nftPalace.addCard(i, 1250 * 10**18, deployer) + assert nftPalace.cards(i) == 1250 * 10**18 + else: + nftPalace.addCard(i, 5000 * 10**18, deployer) + assert nftPalace.cards(i) == 5000 * 10**18 + + +def test_xsdt_stake(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + + XSDT_WHALE = '0xdca628ce9699500191c37d6a4d029bac9ae76447' + xsdt_whale = {'from': XSDT_WHALE} + + xsdt.approve(nftPalace.address, 10**9 * 10**18, {'from': accounts[1]}) + nftPalace.stake(1000 * 10**18, {'from': accounts[1]}) + assert nftPalace.balanceOf(accounts[1]) == 1000 * 10**18 + + xsdt.approve(nftPalace.address, 10**9 * 10**18, {'from': accounts[2]}) + nftPalace.stake(5000 * 10**18, {'from': accounts[2]}) + assert nftPalace.balanceOf(accounts[2]) == 5000 * 10**18 + + xsdt.approve(nftPalace.address, 10**9 * 10**18, {'from': accounts[3]}) + nftPalace.stake(20000 * 10**18, {'from': accounts[3]}) + assert nftPalace.balanceOf(accounts[3]) == 20000 * 10**18 + + xsdt.approve(nftPalace.address, 10**9 * 10**18, {'from': accounts[4]}) + nftPalace.stake(75000 * 10**18, {'from': accounts[4]}) + assert nftPalace.balanceOf(accounts[4]) == 75000 * 10**18 + + +def test_nft_redeem_fail_with_insufficient_points(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + + print("points [1]", nftPalace.earned(accounts[1]) / 10**18) + with brownie.reverts("Not enough points to redeem for card"): + nftPalace.redeem(55, {'from': accounts[1]}) + assert nft.balanceOf(accounts[1], 55) == 0 + + +def test_nft_redeem(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + + # for i in range(1, 24): + # chain.sleep(86400) + # chain.mine(1) + # print("Day", i) + # print(nftPalace.earned(accounts[1]) / 10**18) + # print(nftPalace.earned(accounts[2]) / 10**18) + # print(nftPalace.earned(accounts[3]) / 10**18) + # print(nftPalace.earned(accounts[4]) / 10**18) + + # 1.5 days later + chain.sleep(129600) + chain.mine(1) + + print("points 1.4", nftPalace.earned(accounts[4]) / 10**18) + nftPalace.redeem(55, {'from': accounts[4]}) + assert nft.balanceOf(accounts[4], 55) == 1 + print("URI", nft.uri(55)) + + # 0.5 days later + chain.sleep(43200) + chain.mine(1) + + print("points 2.3", nftPalace.earned(accounts[3]) / 10**18) + nftPalace.redeem(7, {'from': accounts[3]}) + assert nft.balanceOf(accounts[3], 7) == 1 + print("URI", nft.uri(7)) + + # 1.8 days later + chain.sleep(155520) + chain.mine(1) + + print("points 3.2", nftPalace.earned(accounts[2]) / 10**18) + nftPalace.redeem(88, {'from': accounts[2]}) + assert nft.balanceOf(accounts[2], 88) == 1 + print("URI", nft.uri(88)) + + # 1.7 days later + chain.sleep(146880) + chain.mine(1) + + print("points 4.4", nftPalace.earned(accounts[4]) / 10**18) + nftPalace.redeem(101, {'from': accounts[4]}) + assert nft.balanceOf(accounts[4], 101) == 1 + print("URI", nft.uri(101)) + + # 2.6 days later + chain.sleep(224640) + chain.mine(1) + + print("points 5.3", nftPalace.earned(accounts[3]) / 10**18) + nftPalace.redeem(107, {'from': accounts[3]}) + assert nft.balanceOf(accounts[3], 107) == 1 + print("URI", nft.uri(107)) + + # 8 days later + chain.sleep(8 * 86400) + chain.mine(1) + + print("points 6.2", nftPalace.earned(accounts[2]) / 10**18) + nftPalace.redeem(110, {'from': accounts[2]}) + assert nft.balanceOf(accounts[2], 110) == 1 + print("URI", nft.uri(110)) + + # 0 days later + # chain.sleep(2 * 86400) + chain.mine(1) + + print("points 7.1", nftPalace.earned(accounts[1]) / 10**18) + nftPalace.redeem(99, {'from': accounts[1]}) + assert nft.balanceOf(accounts[1], 99) == 1 + print("URI", nft.uri(99)) + + # 6 days later + chain.sleep(6 * 86400) + chain.mine(1) + chain.snapshot() + + print("points 8.4", nftPalace.earned(accounts[4]) / 10**18) + nftPalace.redeem(111, {'from': accounts[4]}) + assert nft.balanceOf(accounts[4], 111) == 1 + print("URI", nft.uri(111)) + + chain.revert() + # 8.4 days later + chain.sleep(725760) + chain.mine(1) + chain.snapshot() + + print("points 9.3", nftPalace.earned(accounts[3]) / 10**18) + nftPalace.redeem(111, {'from': accounts[3]}) + assert nft.balanceOf(accounts[3], 111) == 1 + print("URI", nft.uri(111)) + + chain.revert() + # 33 days later + chain.sleep(33 * 86400) + chain.mine(1) + chain.snapshot() + + print("points 10.2", nftPalace.earned(accounts[2]) / 10**18) + nftPalace.redeem(111, {'from': accounts[2]}) + assert nft.balanceOf(accounts[2], 111) == 1 + print("URI", nft.uri(111)) + + chain.revert() + # 0 days later + # chain.sleep(0) + chain.mine(1) + + print("points 11.1", nftPalace.earned(accounts[1]) / 10**18) + nftPalace.redeem(103, {'from': accounts[1]}) + assert nft.balanceOf(accounts[1], 103) == 1 + print("URI", nft.uri(103)) + + # 181 days later + chain.sleep(15638400) + chain.mine(1) + + print("points 12.1", nftPalace.earned(accounts[1]) / 10**18) + nftPalace.redeem(111, {'from': accounts[1]}) + assert nft.balanceOf(accounts[1], 111) == 1 + print("URI", nft.uri(111)) + + +def test_nft_failed_re_redeem(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + + print("points [4]", nftPalace.earned(accounts[4]) / 10**18) + with brownie.reverts("Max cards minted"): + nftPalace.redeem(111, {'from': accounts[4]}) + assert nft.balanceOf(accounts[4], 111) == 0 + + +def test_nft_redeem_fail_on_uncreated_card(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + + print("points [4]", nftPalace.earned(accounts[4]) / 10**18) + with brownie.reverts("Card not found"): + nftPalace.redeem(222, {'from': accounts[4]}) + assert nft.balanceOf(accounts[4], 222) == 0 + + +def test_nft_redeem_for_whale_exiting_before_redeem(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + VESTING_ESCROW = deployedContracts['VESTING_ESCROW'] + + sdt.transfer(accounts[5], 10**24, {'from': VESTING_ESCROW}) + sdt.approve(xsdt.address, 10**9 * 10**18, {'from': accounts[5]}) + xsdt.enter(10**24, {'from': accounts[5]}) + + xsdt.approve(nftPalace.address, 10**9 * 10**18, {'from': accounts[5]}) + nftPalace.stake(xsdt.balanceOf(accounts[5]), {'from': accounts[5]}) + + chain.sleep(4 * 86400) + chain.mine(1) + print("points [5]", nftPalace.earned(accounts[5]) / 10**18) + + nftPalace.exit({'from': accounts[5]}) + + chain.sleep(3600) + chain.mine(1) + print("points [5]", nftPalace.earned(accounts[5]) / 10**18) + nftPalace.redeem(109, {'from': accounts[5]}) + assert nft.balanceOf(accounts[5], 109) == 1 + + +def test_xsdt_withdraw(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + + before = xsdt.balanceOf(accounts[1]) + nftPalace.withdraw(60 * 10**18, {'from': accounts[1]}) + after = xsdt.balanceOf(accounts[1]) + + assert nftPalace.balanceOf(accounts[1]) == 940 * 10**18 + assert after - before == 60 * 10**18 + + +def test_exit(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + + XSDT_WHALE = '0xdca628ce9699500191c37d6a4d029bac9ae76447' + xsdt_whale = {'from': XSDT_WHALE} + + before = xsdt.balanceOf(accounts[3]) + nftPalace.exit({'from': accounts[3]}) + after = xsdt.balanceOf(accounts[3]) + + assert nftPalace.balanceOf(accounts[3]) == 0 + assert after - before == 20000 * 10**18 + + +def test_set_nft(deployedContracts): + deployer = deployedContracts["deployer"] + xsdt = deployedContracts['xsdt'] + sdt = deployedContracts['sdt'] + nft = deployedContracts['nft'] + nftPalace = deployedContracts['nftPalace'] + + nftPalace.setNFT("0xe4605d46Fd0B3f8329d936a8b258D69276cBa264", deployer) + assert nftPalace.nft() == "0xe4605d46Fd0B3f8329d936a8b258D69276cBa264" diff --git a/protocol/tests/nft/test_nft_booster_vault.py b/protocol/tests/nft/test_nft_booster_vault.py new file mode 100644 index 0000000..b2d78a7 --- /dev/null +++ b/protocol/tests/nft/test_nft_booster_vault.py @@ -0,0 +1,156 @@ +import pytest +from brownie import * +import typer +import json +import brownie + +commonNFT = 1 +rareNFT = 212 +uniqueNFT = 222 + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEPLOYER = accounts[0] + deployer = {'from': DEPLOYER} + + SD_NFT = deployed[ENV]['SD_NFT'] + nft = StakeDaoNFT.at(SD_NFT) + + PROXY_REGISTRY = deployed[ENV]['OPENSEA_PROXY_REGISTRY'] + + nftBoosterVault = NFTBoosterVault.deploy(nft.address, deployer) + + COMMON_OWNER = '0x4b502a08bc54c05772b2c63469e366c2e78459ed' + commonOwner = {'from': COMMON_OWNER} + + RARE_OWNER = '0xb19d9d2949918867340605a971774fe8b7e52c2c' + rareOwner = {'from': RARE_OWNER} + + UNIQUE_OWNER = '0xb19d9d2949918867340605a971774fe8b7e52c2c' + uniqueOwner = {'from': UNIQUE_OWNER} + + TESTER_1 = accounts[1] + tester1 = {'from': TESTER_1} + + TESTER_2 = accounts[2] + tester2 = {'from': TESTER_2} + + nft.safeTransferFrom(COMMON_OWNER, TESTER_1, commonNFT, 1, "", commonOwner) + nft.safeTransferFrom(RARE_OWNER, TESTER_1, rareNFT, 1, "", rareOwner) + nft.safeTransferFrom(UNIQUE_OWNER, TESTER_2, uniqueNFT, 1, "", uniqueOwner) + + nft.setApprovalForAll(nftBoosterVault.address, True, tester1) + nft.setApprovalForAll(nftBoosterVault.address, True, tester2) + + return {"deployer": deployer, + "DEPLOYER": DEPLOYER, + "TESTER_1": TESTER_1, + "tester1": tester1, + "TESTER_2": TESTER_2, + "tester2": tester2, + "PROXY_REGISTRY": PROXY_REGISTRY, + "nft": nft, + "nftBoosterVault": nftBoosterVault} + +def test_constructor(deployedContracts): + nft = deployedContracts["nft"] + nftBoosterVault = deployedContracts["nftBoosterVault"] + DEPLOYER = deployedContracts["DEPLOYER"] + + nftAddress = nftBoosterVault.getNFTAddress() + assert nftAddress == nft.address + + owner = nftBoosterVault.owner() + assert owner == DEPLOYER + +def test_normal_stake_unstake(deployedContracts): + nft = deployedContracts["nft"] + nftBoosterVault = deployedContracts["nftBoosterVault"] + TESTER_1 = deployedContracts["TESTER_1"] + tester1 = deployedContracts["tester1"] + TESTER_2 = deployedContracts["TESTER_2"] + tester2 = deployedContracts["tester2"] + + nftBoosterVault.stake(commonNFT, tester1) + stakedNFT = nftBoosterVault.getStakedNFT(TESTER_1) + assert stakedNFT == commonNFT + + nftBoosterVault.stake(uniqueNFT, tester2) + stakedNFT = nftBoosterVault.getStakedNFT(TESTER_2) + assert stakedNFT == uniqueNFT + + nftBoosterVault.unstake(tester1) + stakedNFT = nftBoosterVault.getStakedNFT(TESTER_1) + assert stakedNFT == 0 + + nftBoosterVault.unstake(tester2) + stakedNFT = nftBoosterVault.getStakedNFT(TESTER_2) + assert stakedNFT == 0 + +def test_double_stake(deployedContracts): + nft = deployedContracts["nft"] + nftBoosterVault = deployedContracts["nftBoosterVault"] + tester1 = deployedContracts["tester1"] + + nftBoosterVault.stake(commonNFT, tester1) + + with brownie.reverts("already staked"): + nftBoosterVault.stake(rareNFT, tester1) + + nftBoosterVault.unstake(tester1) + +def test_unstake_before_stake(deployedContracts): + nft = deployedContracts["nft"] + nftBoosterVault = deployedContracts["nftBoosterVault"] + tester1 = deployedContracts["tester1"] + + with brownie.reverts("not staked"): + nftBoosterVault.unstake(tester1) + +def test_claim_locked(deployedContracts): + nft = deployedContracts["nft"] + nftBoosterVault = deployedContracts["nftBoosterVault"] + tester1 = deployedContracts["tester1"] + TESTER_1 = deployedContracts["TESTER_1"] + tester2 = deployedContracts["tester2"] + TESTER_2 = deployedContracts["TESTER_2"] + deployer = deployedContracts["deployer"] + DEPLOYER = deployedContracts["DEPLOYER"] + + nft.safeTransferFrom(TESTER_1, nftBoosterVault.address, commonNFT, 1, "", tester1) + nft.safeTransferFrom(TESTER_1, nftBoosterVault.address, rareNFT, 1, "", tester1) + nft.safeTransferFrom(TESTER_2, nftBoosterVault.address, uniqueNFT, 1, "", tester2) + + nftBoosterVault.claimLockedNFTs([commonNFT, rareNFT, uniqueNFT], [1, 1, 1], deployer) + + nft.safeTransferFrom(DEPLOYER, TESTER_1, commonNFT, 1, "", deployer) + nft.safeTransferFrom(DEPLOYER, TESTER_1, rareNFT, 1, "", deployer) + nft.safeTransferFrom(DEPLOYER, TESTER_2, uniqueNFT, 1, "", deployer) + +def test_send_random_nft(deployedContracts): + nft = deployedContracts["nft"] + nftBoosterVault = deployedContracts["nftBoosterVault"] + tester1 = deployedContracts["tester1"] + TESTER_1 = deployedContracts["TESTER_1"] + PROXY_REGISTRY = deployedContracts["PROXY_REGISTRY"] + + randNFT = StakeDaoNFT.deploy(PROXY_REGISTRY, tester1) + randNFT.create(1, 1, "", "", tester1) + + with brownie.reverts("nft not accepted"): + randNFT.safeTransferFrom(TESTER_1, nftBoosterVault.address, 1, 1, "", tester1) diff --git a/protocol/tests/nft/test_strat_access_nft.py b/protocol/tests/nft/test_strat_access_nft.py new file mode 100644 index 0000000..fc63a06 --- /dev/null +++ b/protocol/tests/nft/test_strat_access_nft.py @@ -0,0 +1,188 @@ +import pytest +from brownie import * +import typer +import json +import brownie + +MAX_SUPPLY = 1 +INITIAL_SUPPLY = 1 +CREATE_COUNT = 2 +NFT_1 = 223 +NFT_2 = 224 + +@pytest.fixture(scope='module', autouse=True) +def deployedContracts(): + # accounts used for executing transactions + DEPLOYER = accounts[0] + fromDeployer = {'from': DEPLOYER} + + STRATEGY = accounts[1] + fromStrategy = {'from': STRATEGY} + + STRATEGY2 = accounts[2] + fromStrategy2 = {'from': STRATEGY2} + + ALICE = accounts[3] + fromAlice = {'from': ALICE} + + BOB = accounts[4] + fromBob = {'from': BOB} + + # deploy NFT contract + nft = StratAccessNFT.deploy(ZERO_ADDRESS, fromDeployer) + + return {'DEPLOYER': DEPLOYER, 'fromDeployer': fromDeployer, 'nft': nft, 'STRATEGY': STRATEGY, 'fromStrategy': fromStrategy, 'STRATEGY2': STRATEGY2, 'fromStrategy2': fromStrategy2, 'ALICE': ALICE, 'fromAlice': fromAlice, 'BOB': BOB, 'fromBob': fromBob} + +def test_add_strategy(deployedContracts): + fromDeployer = deployedContracts['fromDeployer'] + STRATEGY = deployedContracts['STRATEGY'] + nft = deployedContracts['nft'] + + isStrategy = nft.isStrategy(STRATEGY) + assert isStrategy == False + + nft.addStrategy(STRATEGY, fromDeployer) + + isStrategy = nft.isStrategy(STRATEGY) + assert isStrategy == True + +def test_start_using_without_balance(deployedContracts): + fromStrategy = deployedContracts['fromStrategy'] + ALICE = deployedContracts['ALICE'] + nft = deployedContracts['nft'] + + balance = nft.balanceOf(ALICE, NFT_1) + assert balance == 0 + + with brownie.reverts('StratAccessNFT: user account doesnt have NFT'): + nft.startUsingNFT(ALICE, NFT_1, fromStrategy) + +def test_start_using_with_balance(deployedContracts): + fromStrategy = deployedContracts['fromStrategy'] + fromDeployer = deployedContracts['fromDeployer'] + DEPLOYER = deployedContracts['DEPLOYER'] + ALICE = deployedContracts['ALICE'] + nft = deployedContracts['nft'] + + for i in range(1, CREATE_COUNT + 1): + tx = nft.create(MAX_SUPPLY, INITIAL_SUPPLY, '', '', fromDeployer) + nft.safeBatchTransferFrom(DEPLOYER, ALICE, [NFT_1, NFT_2], [1, 1], '', fromDeployer) + balance = nft.balanceOf(ALICE, NFT_1) + assert balance == 1 + + useCount = nft.getTotalUseCount(ALICE, NFT_1) + assert useCount == 0 + + nft.startUsingNFT(ALICE, NFT_1, fromStrategy) + useCount = nft.getTotalUseCount(ALICE, NFT_1) + assert useCount == 1 + + nft.startUsingNFT(ALICE, NFT_1, fromStrategy) + useCount = nft.getTotalUseCount(ALICE, NFT_1) + assert useCount == 2 + +def test_transfer_when_in_use(deployedContracts): + fromAlice = deployedContracts['fromAlice'] + ALICE = deployedContracts['ALICE'] + BOB = deployedContracts['BOB'] + nft = deployedContracts['nft'] + + useCount = nft.getTotalUseCount(ALICE, NFT_1) + assert useCount > 0 + balance = nft.balanceOf(ALICE, NFT_1) + assert balance == 1 + + # Transfer should fail if NFT is in use + with brownie.reverts('StratAccessNFT: NFT being used in strategy'): + nft.safeTransferFrom(ALICE, BOB, NFT_1, 1, "", fromAlice) + + # Batch transfer should fail if NFT is in use + with brownie.reverts('StratAccessNFT: NFT being used in strategy'): + nft.safeBatchTransferFrom(ALICE, BOB, [NFT_1, NFT_2], [1, 1], "", fromAlice) + +def test_end_using(deployedContracts): + fromStrategy = deployedContracts['fromStrategy'] + ALICE = deployedContracts['ALICE'] + nft = deployedContracts['nft'] + + useCount = nft.getTotalUseCount(ALICE, NFT_1) + assert useCount == 2 + + nft.endUsingNFT(ALICE, NFT_1, fromStrategy) + useCount = nft.getTotalUseCount(ALICE, NFT_1) + assert useCount == 1 + + nft.endUsingNFT(ALICE, NFT_1, fromStrategy) + useCount = nft.getTotalUseCount(ALICE, NFT_1) + assert useCount == 0 + +def test_transfer_when_not_in_use(deployedContracts): + fromAlice = deployedContracts['fromAlice'] + ALICE = deployedContracts['ALICE'] + BOB = deployedContracts['BOB'] + nft = deployedContracts['nft'] + + useCount = nft.getTotalUseCount(ALICE, NFT_1) + assert useCount == 0 + balance = nft.balanceOf(ALICE, NFT_1) + assert balance == 1 + + # Transfer should work if NFT is not in use and final balance is zero + nft.safeTransferFrom(ALICE, BOB, NFT_1, 1, "", fromAlice) + +def test_remove_strategy(deployedContracts): + fromDeployer = deployedContracts['fromDeployer'] + STRATEGY = deployedContracts['STRATEGY'] + nft = deployedContracts['nft'] + + isStrategy = nft.isStrategy(STRATEGY) + assert isStrategy == True + + nft.removeStrategy(STRATEGY, fromDeployer) + + isStrategy = nft.isStrategy(STRATEGY) + assert isStrategy == False + +def test_unregistered_strat(deployedContracts): + fromBob = deployedContracts['fromBob'] + BOB = deployedContracts['BOB'] + ALICE = deployedContracts['ALICE'] + nft = deployedContracts['nft'] + + isStrategy = nft.isStrategy(BOB) + assert isStrategy == False + + # startUsingNFT call from unregistered strategy should fail + with brownie.reverts('StrategyRole: caller does not have the Strategy role'): + nft.startUsingNFT(ALICE, NFT_1, fromBob) + + # endUsingNFT call from unregistered strategy should fail + with brownie.reverts('StrategyRole: caller does not have the Strategy role'): + nft.endUsingNFT(ALICE, NFT_1, fromBob) + +def test_end_using_diff_strat(deployedContracts): + fromStrategy1 = deployedContracts['fromStrategy'] + STRATEGY1 = deployedContracts['STRATEGY'] + fromStrategy2 = deployedContracts['fromStrategy2'] + STRATEGY2 = deployedContracts['STRATEGY2'] + fromDeployer = deployedContracts['fromDeployer'] + fromBob = deployedContracts['fromBob'] + BOB = deployedContracts['BOB'] + ALICE = deployedContracts['ALICE'] + nft = deployedContracts['nft'] + + nft.safeTransferFrom(BOB, ALICE, NFT_1, 1, "", fromBob) + + # add both strategies + nft.addStrategy(STRATEGY1, fromDeployer) + nft.addStrategy(STRATEGY2, fromDeployer) + + # start using NFT from strategy 1 + nft.startUsingNFT(ALICE, NFT_1, fromStrategy1) + + # end using NFT from strategy 2 + with brownie.reverts('SafeMath: subtraction overflow'): + nft.endUsingNFT(ALICE, NFT_1, fromStrategy2) + + # end using NFT from strategy 1 + nft.endUsingNFT(ALICE, NFT_1, fromStrategy1) diff --git a/protocol/tests/polygon/curve/conftest.py b/protocol/tests/polygon/curve/conftest.py new file mode 100644 index 0000000..f8bfe71 --- /dev/null +++ b/protocol/tests/polygon/curve/conftest.py @@ -0,0 +1,62 @@ +import pytest +from brownie import ( + Controller, + StrategyCurveAm3Crv, + GaugeV2, + yVault, + Token, + accounts +) +import json + +STRATEGIES = [ + StrategyCurveAm3Crv +] + +@pytest.fixture(scope="module", autouse=True) +def polygon_config(): + config = open("config.json", "r") + try: + configData = json.loads(config.read()) + except: + configData = {} + + return configData['polygon'] + +@pytest.fixture(scope="module", autouse=True) +def deployerAccount(): + return accounts[0] + +@pytest.fixture(scope="module", autouse=True) +def controller(deployerAccount): + return Controller.deploy(deployerAccount, {'from': deployerAccount}) + +@pytest.fixture(scope="module", autouse=True) +def curveConfig(polygon_config, deployerAccount, controller): + curveConfiguration = {} + + strategyDictionary = { + 'StrategyCurveAm3Crv': StrategyCurveAm3Crv + } + + for key, value in polygon_config['strategies'].items(): + strategy = strategyDictionary[key].deploy(controller, {'from': deployerAccount}) + vaultTokenAddress = value['vaultToken'] + vaultToken = Token.at(vaultTokenAddress) + vault = yVault.deploy(vaultToken, controller, deployerAccount, {'from': deployerAccount}) + gauge = GaugeV2.at(value['gaugeAddress']) + curveConfiguration[key] = {} + curveConfiguration[key]['strategy'] = strategy + curveConfiguration[key]['vaultTokenAddress'] = vaultTokenAddress + curveConfiguration[key]['vaultToken'] = vaultToken + curveConfiguration[key]['vault'] = vault + curveConfiguration[key]['gauge'] = gauge + + setStrategy(controller, vaultTokenAddress, strategy, vault, deployerAccount) + + return curveConfiguration + +def setStrategy(controller, vaultTokenAddress, strategy, vault, deployerAccount): + controller.setVault(vaultTokenAddress, vault, {'from': deployerAccount}) + controller.approveStrategy(vaultTokenAddress, strategy, {'from': deployerAccount}) + controller.setStrategy(vaultTokenAddress, strategy, {'from': deployerAccount}) diff --git a/protocol/tests/polygon/curve/test_am3crv.py b/protocol/tests/polygon/curve/test_am3crv.py new file mode 100644 index 0000000..04010e6 --- /dev/null +++ b/protocol/tests/polygon/curve/test_am3crv.py @@ -0,0 +1,282 @@ +import pytest +from brownie import * +import typer +import json +import requests + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts.load('stakedao-deployer-rug-pull-2') + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + DAI = '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063' + # dai = Contract('0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063') + + VESTING_ESCROW = deployed[ENV]['VESTING_ESCROW'] + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + multisig = {'from': GNOSIS_SAFE_PROXY} + PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A' + + sender = {'from': '0x9f535E3c63Cd1447164BED4933d1efefBbC97a3f'} + + vault = yVault.at('0x7d60F21072b585351dFd5E8b17109458D97ec120') + # 0x91aE00aaC6eE0D7853C8F92710B641F68Cd945Df + controller = Controller.at('0x91aE00aaC6eE0D7853C8F92710B641F68Cd945Df') + old_strat = StrategyCurveAm3Crv.at( + '0x552DAd974da30D67f25BE444991E22CbaE357851') + + new_strat = StrategyAm3Crv.deploy(controller.address, deployer) + + # WHALE = '0x07A75Ba044cDAaa624aAbAD27CB95C42510AF4B5' + # whale = {'from': WHALE} + # vault.deposit(100_000 * 10**18, whale) + GAUGE = '0xe381C25de995d62b453aF8B931aAc84fcCaa7A62' + gauge = GaugeV2.at(GAUGE) + WANT = '0xE7a24EF0C5e95Ffb0f6684b813A78F2a3AD7D171' + want = WMATIC.at(WANT) + _WMATIC = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270' + wmatic = WMATIC.at(_WMATIC) + CRV = '0x172370d5Cd63279eFa6d502DAB29171933a610AF' + crv = WMATIC.at(CRV) + + return {"vault": vault, "controller": controller, "old_strat": old_strat, "want": want, + "multisig": multisig, "deployer": deployer, "wmatic": wmatic, "gauge": gauge, + "sender": sender, "controller": controller, "DAI": DAI, 'new_strat': new_strat, + 'crv': crv} + + +def test_old_strat_harvest(deployedContracts): + want = deployedContracts['want'] + vault = deployedContracts['vault'] + old_strat = deployedContracts['old_strat'] + multisig = deployedContracts['multisig'] + wmatic = deployedContracts['wmatic'] + gauge = deployedContracts['gauge'] + DAI = deployedContracts['DAI'] + deployer = deployedContracts['deployer'] + + totalAm3CrvBefore = old_strat.balanceOf() + poolAm3CrvBefore = old_strat.balanceOfPool() + + stratWmaticInside = wmatic.balanceOf(old_strat.address) + stratWmaticClaimable = gauge.reward_integral_for( + wmatic.address, old_strat.address) + + print("Inside, Claimable", stratWmaticInside, stratWmaticClaimable) + + data = create_swapdata(wmatic.address, DAI, stratWmaticInside + + stratWmaticClaimable, old_strat.address) + + old_strat.harvest(data, deployer) + + totalAm3CrvAfter = old_strat.balanceOf() + poolAm3CrvAfter = old_strat.balanceOfPool() + + print("Total am3Crv increase", (totalAm3CrvAfter - totalAm3CrvBefore) / 10**18) + print("Pool am3Crv increase", (poolAm3CrvAfter - poolAm3CrvBefore) / 10**18) + + assert totalAm3CrvAfter - totalAm3CrvBefore > 0 + assert poolAm3CrvAfter - poolAm3CrvBefore > 0 + + +def test_crv_rescue(deployedContracts): + want = deployedContracts['want'] + vault = deployedContracts['vault'] + old_strat = deployedContracts['old_strat'] + multisig = deployedContracts['multisig'] + wmatic = deployedContracts['wmatic'] + gauge = deployedContracts['gauge'] + DAI = deployedContracts['DAI'] + deployer = deployedContracts['deployer'] + controller = deployedContracts['controller'] + crv = deployedContracts['crv'] + + oldStratCrvBefore = crv.balanceOf(old_strat.address) + deployerCrvBefore = crv.balanceOf( + '0xb36a0671B3D49587236d7833B01E79798175875f') + + controller.inCaseStrategyTokenGetStuck( + old_strat.address, crv.address, deployer) + controller.inCaseTokensGetStuck( + crv.address, crv.balanceOf(controller.address), deployer) + + oldStratCrvAfter = crv.balanceOf(old_strat.address) + deployerCrvAfter = crv.balanceOf( + '0xb36a0671B3D49587236d7833B01E79798175875f') + + print("strat diff", (oldStratCrvAfter - oldStratCrvBefore) / 10**18) + print("deployer diff", (deployerCrvAfter - deployerCrvBefore) / 10**18) + + +def test_migrate_strat(deployedContracts): + want = deployedContracts['want'] + vault = deployedContracts['vault'] + old_strat = deployedContracts['old_strat'] + multisig = deployedContracts['multisig'] + wmatic = deployedContracts['wmatic'] + gauge = deployedContracts['gauge'] + DAI = deployedContracts['DAI'] + deployer = deployedContracts['deployer'] + controller = deployedContracts['controller'] + new_strat = deployedContracts['new_strat'] + + vaultBefore = want.balanceOf(vault.address) + oldStratBefore = old_strat.balanceOf() + + controller.approveStrategy(want.address, new_strat.address, deployer) + controller.setStrategy(want.address, new_strat.address, deployer) + + vaultAfter = want.balanceOf(vault.address) + + assert vaultAfter == vaultBefore + oldStratBefore + + +def test_new_strat_earn(deployedContracts): + want = deployedContracts['want'] + vault = deployedContracts['vault'] + old_strat = deployedContracts['old_strat'] + multisig = deployedContracts['multisig'] + wmatic = deployedContracts['wmatic'] + gauge = deployedContracts['gauge'] + DAI = deployedContracts['DAI'] + deployer = deployedContracts['deployer'] + controller = deployedContracts['controller'] + new_strat = deployedContracts['new_strat'] + + vaultBefore = want.balanceOf(vault.address) + oldStratBefore = old_strat.balanceOf() + + vault.earn(deployer) + vault.earn(deployer) + vault.earn(deployer) + + vaultAfter = want.balanceOf(vault.address) + newStratAfter = new_strat.balanceOf() + + assert vaultBefore == newStratAfter + vaultAfter + + +def test_new_strat_harvest(deployedContracts): + want = deployedContracts['want'] + vault = deployedContracts['vault'] + old_strat = deployedContracts['old_strat'] + multisig = deployedContracts['multisig'] + wmatic = deployedContracts['wmatic'] + gauge = deployedContracts['gauge'] + DAI = deployedContracts['DAI'] + deployer = deployedContracts['deployer'] + controller = deployedContracts['controller'] + new_strat = deployedContracts['new_strat'] + crv = deployedContracts['crv'] + + chain.sleep(5 * 86400) + chain.mine(100) + + # send old_strat's crv rewards to new_strat for compounding + crv.transfer(new_strat.address, crv.balanceOf( + '0xb36a0671B3D49587236d7833B01E79798175875f'), deployer) + + stratWmaticInside = wmatic.balanceOf(new_strat.address) + stratWmaticClaimable = gauge.reward_integral_for( + wmatic.address, new_strat.address) + + stratCrvInside = crv.balanceOf(new_strat.address) + stratCrvClaimable = gauge.reward_integral_for( + crv.address, new_strat.address) + + print("Wmatic: Inside, Claimable", stratWmaticInside, stratWmaticClaimable) + print("Crv: Inside, Claimable", stratCrvInside, stratCrvClaimable) + + data_wmatic = create_swapdata(wmatic.address, DAI, stratWmaticInside + + stratWmaticClaimable, new_strat.address) + data_crv = create_swapdata(crv.address, DAI, stratCrvInside + + stratCrvClaimable, new_strat.address) + + totalAm3CrvBefore = new_strat.balanceOf() + poolAm3CrvBefore = new_strat.balanceOfPool() + + new_strat.harvest(data_wmatic, data_crv, deployer) + + totalAm3CrvAfter = new_strat.balanceOf() + poolAm3CrvAfter = new_strat.balanceOfPool() + + print("Total am3Crv increase", (totalAm3CrvAfter - totalAm3CrvBefore) / 10**18) + print("Pool am3Crv increase", (poolAm3CrvAfter - poolAm3CrvBefore) / 10**18) + + +def test_new_strat_withdraw(deployedContracts): + want = deployedContracts['want'] + vault = deployedContracts['vault'] + old_strat = deployedContracts['old_strat'] + multisig = deployedContracts['multisig'] + wmatic = deployedContracts['wmatic'] + gauge = deployedContracts['gauge'] + DAI = deployedContracts['DAI'] + deployer = deployedContracts['deployer'] + controller = deployedContracts['controller'] + new_strat = deployedContracts['new_strat'] + crv = deployedContracts['crv'] + + USER = '0xf10806C57b21F89A60ac5b743532fd3d4efcE18A' + + userBefore = want.balanceOf(USER) + vault.withdraw(vault.balanceOf(USER), {'from': USER}) + userAfter = want.balanceOf(USER) + + print("User increase", (userAfter - userBefore) / 10**18) + + +def create_swapdata(_from, to, amount, strat): + # be wary that if someone withdraws right before harvest, + # then stratWmaticInside + stratWmaticClaimable might change + URL = "https://apiv4.paraswap.io/v2/prices/" + PARAMS = { + 'from': _from, + 'to': to, + 'fromDecimals': 18, + 'toDecimals': 18, + 'amount': amount, + 'side': 'SELL', + 'network': 137 + } + r = requests.get(url=URL, params=PARAMS) + data = r.json() + # print('PRICE_ROUTE', data['priceRoute']) + priceRoute = data['priceRoute'] + + print('tokenFrom ', 'tokenTo ', 'priceWithSlippage ', + priceRoute['details']['tokenFrom'], priceRoute['details']['tokenTo'], priceRoute['priceWithSlippage']) + + POST_URL = "https://apiv4.paraswap.io/v2/transactions/137?skipChecks=true" + POST_DATA = { + 'userAddress': strat, + 'srcToken': priceRoute['details']['tokenFrom'], + 'destToken': priceRoute['details']['tokenTo'], + 'srcAmount': priceRoute['details']['srcAmount'], + 'destAmount': priceRoute['priceWithSlippage'], + 'toDecimals': 18, + 'fromDecimals': 18, + 'referrer': 'stakedao', + 'priceRoute': priceRoute + } + r = requests.post(url=POST_URL, json=POST_DATA) + print("RRRR", r) + response = r.json() + return response['data'] diff --git a/protocol/tests/polygon/curve/test_btcCrv.py b/protocol/tests/polygon/curve/test_btcCrv.py new file mode 100644 index 0000000..98b7fc9 --- /dev/null +++ b/protocol/tests/polygon/curve/test_btcCrv.py @@ -0,0 +1,299 @@ +import pytest +from brownie import * +import typer +import json +import requests + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts.load('stakedao-deployer-rug-pull-2') + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + DAI = '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063' + # dai = Contract('0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063') + WBTC = '0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6' + # wbtc = WMATIC.at(WBTC) + + VESTING_ESCROW = deployed[ENV]['VESTING_ESCROW'] + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + multisig = {'from': GNOSIS_SAFE_PROXY} + PROXY = '0xF34Ae3C7515511E29d8Afe321E67Bdf97a274f1A' + + sender = {'from': '0x9f535E3c63Cd1447164BED4933d1efefBbC97a3f'} + + WANT = '0xf8a57c1d3b9629b77b6726a042ca48990A84Fb49' + want = WMATIC.at(WANT) + + # vault = yVault.at('0x7d60F21072b585351dFd5E8b17109458D97ec120') + vault = Vault.deploy( + WANT, '0x91aE00aaC6eE0D7853C8F92710B641F68Cd945Df', DEFAULT_DEPLOYER_ACCOUNT, deployer) + # 0x91aE00aaC6eE0D7853C8F92710B641F68Cd945Df + controller = Controller.at('0x91aE00aaC6eE0D7853C8F92710B641F68Cd945Df') + old_strat = StrategyCurveAm3Crv.at( + '0x552DAd974da30D67f25BE444991E22CbaE357851') + + new_strat = StrategyBtcCurve.deploy(controller.address, deployer) + + WHALE = '0x1dDc97E6Ce44Dd86E88844861dC4525E50F316C0' + whale = {'from': WHALE} + # vault.deposit(100_000 * 10**18, whale) + GAUGE = '0xffbACcE0CC7C19d46132f1258FC16CF6871D153c' + gauge = GaugeV2.at(GAUGE) + _WMATIC = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270' + wmatic = WMATIC.at(_WMATIC) + CRV = '0x172370d5Cd63279eFa6d502DAB29171933a610AF' + crv = WMATIC.at(CRV) + + controller.setVault(WANT, vault.address, deployer) + controller.approveStrategy(WANT, new_strat.address, deployer) + controller.setStrategy(WANT, new_strat.address, deployer) + + return {"vault": vault, "controller": controller, "old_strat": old_strat, "want": want, + "multisig": multisig, "deployer": deployer, "wmatic": wmatic, "gauge": gauge, + "sender": sender, "controller": controller, "DAI": DAI, 'new_strat': new_strat, + 'crv': crv, 'whale': whale, 'WBTC': WBTC} + + +def test_new_strat_earn(deployedContracts): + want = deployedContracts['want'] + vault = deployedContracts['vault'] + old_strat = deployedContracts['old_strat'] + multisig = deployedContracts['multisig'] + wmatic = deployedContracts['wmatic'] + gauge = deployedContracts['gauge'] + DAI = deployedContracts['DAI'] + deployer = deployedContracts['deployer'] + controller = deployedContracts['controller'] + new_strat = deployedContracts['new_strat'] + whale = deployedContracts['whale'] + + # want.transfer(vault.address, 3 * 10**18, whale) + want.approve(vault.address, 100 * 10**18, whale) + vault.deposit(3 * 10**18, whale) + + vaultBefore = want.balanceOf(vault.address) + oldStratBefore = old_strat.balanceOf() + + vault.earn(deployer) + # vault.earn(deployer) + # vault.earn(deployer) + + vaultAfter = want.balanceOf(vault.address) + newStratAfter = new_strat.balanceOf() + + assert vaultBefore == newStratAfter + vaultAfter + + +def test_new_strat_harvest(deployedContracts): + want = deployedContracts['want'] + vault = deployedContracts['vault'] + old_strat = deployedContracts['old_strat'] + multisig = deployedContracts['multisig'] + wmatic = deployedContracts['wmatic'] + gauge = deployedContracts['gauge'] + DAI = deployedContracts['DAI'] + deployer = deployedContracts['deployer'] + controller = deployedContracts['controller'] + new_strat = deployedContracts['new_strat'] + crv = deployedContracts['crv'] + WBTC = deployedContracts['WBTC'] + + chain.sleep(5 * 86400) + chain.mine(100) + + # send old_strat's crv rewards to new_strat for compounding + # crv.transfer(new_strat.address, crv.balanceOf( + # '0xb36a0671B3D49587236d7833B01E79798175875f'), deployer) + + stratWmaticInside = wmatic.balanceOf(new_strat.address) + stratWmaticClaimable = gauge.reward_integral_for( + wmatic.address, new_strat.address) + + stratCrvInside = crv.balanceOf(new_strat.address) + stratCrvClaimable = gauge.reward_integral_for( + crv.address, new_strat.address) + + print("Wmatic: Inside, Claimable", stratWmaticInside, + stratWmaticClaimable / 10**18) + print("Crv: Inside, Claimable", stratCrvInside, stratCrvClaimable / 10**18) + + data_wmatic = create_swapdata(wmatic.address, WBTC, stratWmaticInside + + stratWmaticClaimable, new_strat.address) + data_crv = create_swapdata(crv.address, WBTC, stratCrvInside + + stratCrvClaimable, new_strat.address) + + totalAm3CrvBefore = new_strat.balanceOf() + poolAm3CrvBefore = new_strat.balanceOfPool() + + new_strat.harvest(data_wmatic, data_crv, deployer) + + totalAm3CrvAfter = new_strat.balanceOf() + poolAm3CrvAfter = new_strat.balanceOfPool() + + print("Total btcCrv increase", (totalAm3CrvAfter - totalAm3CrvBefore) / 10**18) + print("Pool btcCrv increase", (poolAm3CrvAfter - poolAm3CrvBefore) / 10**18) + + +def test_new_strat_withdraw(deployedContracts): + want = deployedContracts['want'] + vault = deployedContracts['vault'] + old_strat = deployedContracts['old_strat'] + multisig = deployedContracts['multisig'] + wmatic = deployedContracts['wmatic'] + gauge = deployedContracts['gauge'] + DAI = deployedContracts['DAI'] + deployer = deployedContracts['deployer'] + controller = deployedContracts['controller'] + new_strat = deployedContracts['new_strat'] + crv = deployedContracts['crv'] + + WHALE = '0x1dDc97E6Ce44Dd86E88844861dC4525E50F316C0' + USER = '0xf10806C57b21F89A60ac5b743532fd3d4efcE18A' + + userBefore = want.balanceOf(WHALE) + vault.withdraw(vault.balanceOf(WHALE), {'from': WHALE}) + userAfter = want.balanceOf(WHALE) + + print("User increase", (userAfter - userBefore) / 10**18) + + +def create_swapdata(_from, to, amount, strat): + # be wary that if someone withdraws right before harvest, + # then stratWmaticInside + stratWmaticClaimable might change + URL = "https://apiv4.paraswap.io/v2/prices/" + PARAMS = { + 'from': _from, + 'to': to, + 'fromDecimals': 18, + 'toDecimals': 18, + 'amount': amount, + 'side': 'SELL', + 'network': 137 + } + r = requests.get(url=URL, params=PARAMS) + data = r.json() + # print('PRICE_ROUTE', data['priceRoute']) + priceRoute = data['priceRoute'] + + print('tokenFrom ', 'tokenTo ', 'priceWithSlippage ', + priceRoute['details']['tokenFrom'], priceRoute['details']['tokenTo'], priceRoute['priceWithSlippage']) + + POST_URL = "https://apiv4.paraswap.io/v2/transactions/137?skipChecks=true" + POST_DATA = { + 'userAddress': strat, + 'srcToken': priceRoute['details']['tokenFrom'], + 'destToken': priceRoute['details']['tokenTo'], + 'srcAmount': priceRoute['details']['srcAmount'], + 'destAmount': priceRoute['priceWithSlippage'], + 'toDecimals': 18, + 'fromDecimals': 18, + 'referrer': 'stakedao', + 'priceRoute': priceRoute + } + r = requests.post(url=POST_URL, json=POST_DATA) + print("RRRR", r) + response = r.json() + return response['data'] + + +# def test_old_strat_harvest(deployedContracts): +# want = deployedContracts['want'] +# vault = deployedContracts['vault'] +# old_strat = deployedContracts['old_strat'] +# multisig = deployedContracts['multisig'] +# wmatic = deployedContracts['wmatic'] +# gauge = deployedContracts['gauge'] +# DAI = deployedContracts['DAI'] +# deployer = deployedContracts['deployer'] + +# totalAm3CrvBefore = old_strat.balanceOf() +# poolAm3CrvBefore = old_strat.balanceOfPool() + +# stratWmaticInside = wmatic.balanceOf(old_strat.address) +# stratWmaticClaimable = gauge.reward_integral_for( +# wmatic.address, old_strat.address) + +# print("Inside, Claimable", stratWmaticInside, stratWmaticClaimable) + +# data = create_swapdata(wmatic.address, DAI, stratWmaticInside + +# stratWmaticClaimable, old_strat.address) + +# old_strat.harvest(data, deployer) + +# totalAm3CrvAfter = old_strat.balanceOf() +# poolAm3CrvAfter = old_strat.balanceOfPool() + +# print("Total am3Crv increase", (totalAm3CrvAfter - totalAm3CrvBefore) / 10**18) +# print("Pool am3Crv increase", (poolAm3CrvAfter - poolAm3CrvBefore) / 10**18) + +# assert totalAm3CrvAfter - totalAm3CrvBefore > 0 +# assert poolAm3CrvAfter - poolAm3CrvBefore > 0 + + +# def test_crv_rescue(deployedContracts): +# want = deployedContracts['want'] +# vault = deployedContracts['vault'] +# old_strat = deployedContracts['old_strat'] +# multisig = deployedContracts['multisig'] +# wmatic = deployedContracts['wmatic'] +# gauge = deployedContracts['gauge'] +# DAI = deployedContracts['DAI'] +# deployer = deployedContracts['deployer'] +# controller = deployedContracts['controller'] +# crv = deployedContracts['crv'] + +# oldStratCrvBefore = crv.balanceOf(old_strat.address) +# deployerCrvBefore = crv.balanceOf( +# '0xb36a0671B3D49587236d7833B01E79798175875f') + +# controller.inCaseStrategyTokenGetStuck( +# old_strat.address, crv.address, deployer) +# controller.inCaseTokensGetStuck( +# crv.address, crv.balanceOf(controller.address), deployer) + +# oldStratCrvAfter = crv.balanceOf(old_strat.address) +# deployerCrvAfter = crv.balanceOf( +# '0xb36a0671B3D49587236d7833B01E79798175875f') + +# print("strat diff", (oldStratCrvAfter - oldStratCrvBefore) / 10**18) +# print("deployer diff", (deployerCrvAfter - deployerCrvBefore) / 10**18) + + +# def test_migrate_strat(deployedContracts): +# want = deployedContracts['want'] +# vault = deployedContracts['vault'] +# old_strat = deployedContracts['old_strat'] +# multisig = deployedContracts['multisig'] +# wmatic = deployedContracts['wmatic'] +# gauge = deployedContracts['gauge'] +# DAI = deployedContracts['DAI'] +# deployer = deployedContracts['deployer'] +# controller = deployedContracts['controller'] +# new_strat = deployedContracts['new_strat'] + +# vaultBefore = want.balanceOf(vault.address) +# oldStratBefore = old_strat.balanceOf() + +# controller.approveStrategy(want.address, new_strat.address, deployer) +# controller.setStrategy(want.address, new_strat.address, deployer) + +# vaultAfter = want.balanceOf(vault.address) + +# assert vaultAfter == vaultBefore + oldStratBefore diff --git a/protocol/tests/polygon/curve/test_curve.py b/protocol/tests/polygon/curve/test_curve.py new file mode 100644 index 0000000..69329d2 --- /dev/null +++ b/protocol/tests/polygon/curve/test_curve.py @@ -0,0 +1,97 @@ +import pytest + +strategyNames = [ + "StrategyCurveAm3Crv" +] + +@pytest.mark.parametrize("strategyName", strategyNames) +def test_polygon_vault_deposit(curveConfig, strategyName, deployerAccount): + token = curveConfig[strategyName]['vaultToken'] + vault = curveConfig[strategyName]['vault'] + gauge = curveConfig[strategyName]['gauge'] + + amount = 100_000*10**18 + verifyDeposit(amount, vault, deployerAccount, gauge, token) + +@pytest.mark.parametrize("strategyName", strategyNames) +def test_polygon_vault_withdraw(curveConfig, strategyName, deployerAccount): + token = curveConfig[strategyName]['vaultToken'] + vault = curveConfig[strategyName]['vault'] + gauge = curveConfig[strategyName]['gauge'] + + amount = 100_000*10**18 + verifyDeposit(amount, vault, deployerAccount, gauge, token) + verifyWithdraw(vault, token, deployerAccount) + +@pytest.mark.parametrize("strategyName", strategyNames) +def test_polygon_vault_earn(curveConfig, strategyName, deployerAccount): + token = curveConfig[strategyName]['vaultToken'] + vault = curveConfig[strategyName]['vault'] + strategy = curveConfig[strategyName]['strategy'] + gauge = curveConfig[strategyName]['gauge'] + + amount = 100_000*10**18 + verifyDeposit(amount, vault, deployerAccount, gauge, token) + verifyEarn(strategy, vault, deployerAccount) + +@pytest.mark.parametrize("strategyName", strategyNames) +def test_polygon_strategy_whale_process(curveConfig, strategyName, deployerAccount): + token = curveConfig[strategyName]['vaultToken'] + vault = curveConfig[strategyName]['vault'] + strategy = curveConfig[strategyName]['strategy'] + gauge = curveConfig[strategyName]['gauge'] + amount = 100_000_000_000 * 10**18 + + verifyDeposit(amount, vault, deployerAccount, gauge, token) + verifyEarn(strategy, vault, deployerAccount) + verifyWithdraw(vault, token, deployerAccount) + +def seedAccount(token, approvalAccount, amount, receivingAccount, gauge): + token.approve(approvalAccount, amount, {'from': receivingAccount}) + token.transfer(receivingAccount, amount, {'from': gauge}) + +def verifyDeposit(amount, vault, deployerAccount, gauge, token): + seedAccount(token, vault, amount, deployerAccount, gauge) + + senderShareBefore = vault.balanceOf(deployerAccount) + senderBalanceBefore = token.balanceOf(deployerAccount) + vaultBalanceBefore = token.balanceOf(vault.address) + + token.approve(vault, senderBalanceBefore, {'from': deployerAccount}) + vault.deposit(senderBalanceBefore, {'from': deployerAccount}) + + senderShareAfter = vault.balanceOf(deployerAccount) + senderBalanceAfter = token.balanceOf(deployerAccount) + vaultBalanceAfter = token.balanceOf(vault.address) + + assert senderShareAfter - senderShareBefore > 0 + assert senderBalanceBefore - senderBalanceAfter > 0 + assert senderBalanceBefore - senderBalanceAfter == vaultBalanceAfter - vaultBalanceBefore + +def verifyWithdraw(vault, token, deployerAccount): + senderShareBefore = vault.balanceOf(deployerAccount) + senderBalanceBefore = token.balanceOf(deployerAccount) + vaultBalanceBefore = token.balanceOf(vault.address) + + vault.withdraw(vaultBalanceBefore, {'from': deployerAccount}) + + senderShareAfter = vault.balanceOf(deployerAccount) + senderBalanceAfter = token.balanceOf(deployerAccount) + vaultBalanceAfter = token.balanceOf(vault.address) + + assert senderShareAfter - senderShareBefore < 0 + assert senderBalanceBefore - senderBalanceAfter < 0 + assert senderBalanceBefore - senderBalanceAfter == vaultBalanceAfter - vaultBalanceBefore + +def verifyEarn(strategy, vault, deployerAccount): + strategyBalanceBefore = strategy.balanceOf() + totalBalanceBefore = vault.balance() + + vault.earn({'from': deployerAccount}) + + strategyBalanceAfter = strategy.balanceOf() + totalBalanceAfter = vault.balance() + + assert strategyBalanceAfter > strategyBalanceBefore + assert totalBalanceAfter == totalBalanceBefore + \ No newline at end of file diff --git a/protocol/tests/sdt-claim-distribution/test_sdt_claim.py b/protocol/tests/sdt-claim-distribution/test_sdt_claim.py new file mode 100644 index 0000000..4cfb536 --- /dev/null +++ b/protocol/tests/sdt-claim-distribution/test_sdt_claim.py @@ -0,0 +1,77 @@ +import pytest +from brownie import * +import typer +import json +import brownie + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + merkleDistribution = open("merkle-distribution.json", "r") + try: + merkleDistribution = json.loads(merkleDistribution.read()) + except: + merkleDistribution = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts[0] + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + distributor = MerkleDistributorSdt.deploy( + DEFAULT_DEPLOYER_ACCOUNT, + DEFAULT_DEPLOYER_ACCOUNT, + deployer + ) + distributor.proposeMerkleRoot(merkleDistribution["merkleRoot"], deployer) + distributor.reviewPendingMerkleRoot(True, deployer) + + SDT = deployed[ENV]['SDT'] + sdt = Contract(SDT) + + SDT_WHALE = '0xc78fa2af0ca7990bb5ff32c9a728125be58cf247' + sdtWhale = {'from': SDT_WHALE} + + bal = sdt.balanceOf(SDT_WHALE) + print('SDT_WHALE', bal) + print('tokenTotal', merkleDistribution["tokenTotal"]) + + sdt.transfer(distributor.address, merkleDistribution["tokenTotal"], sdtWhale) + + return { + "deployer": deployer, + "SDT": SDT, + "sdt": sdt, + "distributor": distributor, + "merkleDistribution": merkleDistribution + } + + +def test_claim(deployedContracts): + sdt = deployedContracts['sdt'] + distributor = deployedContracts['distributor'] + merkleDistribution = deployedContracts['merkleDistribution'] + + CLAIM0 = merkleDistribution["claims"][0] + CLAIMER = CLAIM0["address"] + claimer = {"from": CLAIMER} + + bal = sdt.balanceOf(CLAIMER) + + distributor.claim(0, CLAIM0["index"], CLAIM0["amount"], CLAIM0["proof"], claimer) + + bal2 = sdt.balanceOf(CLAIMER) + print(bal2 - bal, CLAIM0["amount"]) + assert (bal2 - bal == CLAIM0["amount"]) diff --git a/protocol/tests/staking/test_staking.py b/protocol/tests/staking/test_staking.py new file mode 100644 index 0000000..e3abb9f --- /dev/null +++ b/protocol/tests/staking/test_staking.py @@ -0,0 +1,620 @@ +import pytest +from brownie import * +import typer +import json + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts[0] + + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + TREASURY_VAULT = deployed[ENV]['TREASURY_VAULT'] + treasuryVault = Contract(TREASURY_VAULT) + + SDT = deployed[ENV]['SDT'] + sdt = Contract(SDT) + + SBTC_CRV = deployed[ENV]['SBTC_CRV'] + + THREE_CRV = deployed[ENV]['THREE_CRV'] + + EURS_CRV = deployed[ENV]['EURS_CRV'] + + DAI = deployed[ENV]['DAI'] + dai = Contract(DAI) + + VESTING_ESCROW = deployed[ENV]['VESTING_ESCROW'] + + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + SDT_STAKE_AMOUNT = 100*10**18 + + sdt.transfer(DEFAULT_DEPLOYER_ACCOUNT, + 10**6 * 10**18, {'from': VESTING_ESCROW}) + sdt.transfer(accounts[1], 10**6 * 10**18, {'from': VESTING_ESCROW}) + sdt.transfer(accounts[2], 2 * 10**6 * 10**18, {'from': VESTING_ESCROW}) + sdt.transfer(accounts[3], 10**6 * 10**18, {'from': VESTING_ESCROW}) + + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + multisig = {'from': GNOSIS_SAFE_PROXY} + + # deploy NFT contract + # cardNft = ERC1155Tradable.deploy("SDT NFT Cards", "SDTC", ZERO_ADDRESS, deployer) + + # deploy SdtPool + # sdtPool = SdtPool.deploy(cardNft.address, sdt.address, DAI, deployer) + + # deploy Sanctuary + sanctuary = Sanctuary.deploy(sdt.address, deployer) + + # set setRewardDistribution as TreasuryVault + # sdtPool.setRewardDistribution(treasuryVault.address, deployer) + + # give sdtPool a minter role + # cardNft.addMinter(sdtPool.address, deployer) + + # call setGovernanceStaking on Treasury Vault + treasuryVault.setGovernanceStaking(sanctuary.address, multisig) + + # set multisig as an authorized address for Treasuty Vault + treasuryVault.setAuthorized(GNOSIS_SAFE_PROXY, multisig) + + return {"dai": dai, "treasuryVault": treasuryVault, "multisig": multisig, "deployer": deployer, "DAI": DAI, "SDT": SDT, "SBTC_CRV": SBTC_CRV, "THREE_CRV": THREE_CRV, "EURS_CRV": EURS_CRV, "SDT_STAKE_AMOUNT": SDT_STAKE_AMOUNT, "sdt": sdt, "sanctuary": sanctuary} + + +def test_swap_tokens_for_sdt(deployedContracts): + # swap treasury fees for dai + treasuryVault = deployedContracts["treasuryVault"] + deployer = deployedContracts["deployer"] + multisig = deployedContracts["multisig"] + SBTC_CRV = deployedContracts["SBTC_CRV"] + THREE_CRV = deployedContracts["THREE_CRV"] + EURS_CRV = deployedContracts["EURS_CRV"] + DAI = deployedContracts["DAI"] + sanctuary = deployedContracts['sanctuary'] + sdt = deployedContracts['sdt'] + + treasuryVault.setRewards(sdt.address, multisig) + rewardToken = treasuryVault.rewards() + + assert rewardToken == sdt.address + + # beforeSbtcCrvBal = Contract(SBTC_CRV).balanceOf(treasuryVault.address) + # beforeThreeCrvBal = Contract(THREE_CRV).balanceOf(treasuryVault.address) + beforeEursCrvBal = Contract(EURS_CRV).balanceOf(treasuryVault.address) + + # assert beforeSbtcCrvBal > 0 + # assert beforeThreeCrvBal > 0 + assert beforeEursCrvBal > 0 + + assert Contract(rewardToken).balanceOf(treasuryVault.address) == 0 + + # treasuryVault.swapViaZap(SBTC_CRV, rewardToken, beforeSbtcCrvBal, multisig) + # treasuryVault.swapViaZap(THREE_CRV, rewardToken, + # beforeThreeCrvBal, multisig) + treasuryVault.swapViaZap(EURS_CRV, rewardToken, beforeEursCrvBal, multisig) + treasuryVault.convert(EURS_CRV, rewardToken, beforeEursCrvBal, multisig) + + # assert Contract(SBTC_CRV).balanceOf(treasuryVault.address) == 0 + # assert Contract(THREE_CRV).balanceOf(treasuryVault.address) == 0 + assert Contract(EURS_CRV).balanceOf(treasuryVault.address) == 0 + treasurySDTBal = Contract(rewardToken).balanceOf(treasuryVault.address) + print("treasurySDTBal", treasurySDTBal / 10**18) + assert treasurySDTBal > 0 + + +def test_stake_sdt_on_empty_sanctuary(deployedContracts): + sdt = deployedContracts["sdt"] + sanctuary = deployedContracts['sanctuary'] + deployer = deployedContracts["deployer"] + SDT_STAKE_AMOUNT = deployedContracts["SDT_STAKE_AMOUNT"] + treasuryVault = deployedContracts["treasuryVault"] + + user0xSDTBefore = sanctuary.balanceOf(deployer["from"]) + user0SDTBefore = sdt.balanceOf(deployer['from']) + user1xSDTBefore = sanctuary.balanceOf(accounts[1]) + user1SDTBefore = sdt.balanceOf(accounts[1]) + user2xSDTBefore = sanctuary.balanceOf(accounts[2]) + user2SDTBefore = sdt.balanceOf(accounts[2]) + sanctuaryxSDTBefore = sanctuary.totalSupply() + sanctuarySDTBefore = sdt.balanceOf(sanctuary.address) + print() + print("STAKE") + print('user0xSDTBefore', user0xSDTBefore / 10**18) + print('user0SDTBefore', user0SDTBefore / 10**18) + print('user1xSDTBefore', user1xSDTBefore / 10**18) + print('user1SDTBefore', user1SDTBefore / 10**18) + print('user2xSDTBefore', user2xSDTBefore / 10**18) + print('user2SDTBefore', user2SDTBefore / 10**18) + print('sanctuaryxSDTBefore', sanctuaryxSDTBefore / 10**18) + print('sanctuarySDTBefore', sanctuarySDTBefore / 10**18) + print() + # approve and stake sdt + sdt.approve(sanctuary.address, 10**9 * 10**18, deployer) + sdt.approve(sanctuary.address, 10**9 * 10**18, {'from': accounts[1]}) + sdt.approve(sanctuary.address, 10**9 * 10**18, {'from': accounts[2]}) + sanctuary.enter(SDT_STAKE_AMOUNT, deployer) + sanctuary.enter(1000 * 10**18, {'from': accounts[1]}) + sanctuary.enter(1000000 * 10**18, {'from': accounts[2]}) + + user0xSDTAfter = sanctuary.balanceOf(deployer["from"]) + user0SDTAfter = sdt.balanceOf(deployer['from']) + user1xSDTAfter = sanctuary.balanceOf(accounts[1]) + user1SDTAfter = sdt.balanceOf(accounts[1]) + user2xSDTAfter = sanctuary.balanceOf(accounts[2]) + user2SDTAfter = sdt.balanceOf(accounts[2]) + sanctuaryxSDTAfter = sanctuary.totalSupply() + sanctuarySDTAfter = sdt.balanceOf(sanctuary.address) + print() + print('user0xSDTAfter', user0xSDTAfter / 10**18) + print('user0SDTAfter', user0SDTAfter / 10**18) + print('user1xSDTAfter', user1xSDTAfter / 10**18) + print('user1SDTAfter', user1SDTAfter / 10**18) + print('user2xSDTAfter', user2xSDTAfter / 10**18) + print('user2SDTAfter', user2SDTAfter / 10**18) + print('sanctuaryxSDTAfter', sanctuaryxSDTAfter / 10**18) + print('sanctuarySDTAfter', sanctuarySDTAfter / 10**18) + print() + + assert sanctuaryxSDTAfter - sanctuaryxSDTBefore == user0xSDTAfter - \ + user0xSDTBefore + user1xSDTAfter - \ + user1xSDTBefore + user2xSDTAfter - user2xSDTBefore + assert user0SDTBefore - user0SDTAfter + user1SDTBefore - user1SDTAfter + \ + user2SDTBefore - user2SDTAfter == sanctuarySDTAfter - sanctuarySDTBefore + + +def test_deposit_sdt_to_sanctuary(deployedContracts): + # call toGovernanceStaking from treasury + deployer = deployedContracts["deployer"] + multisig = deployedContracts["multisig"] + treasuryVault = deployedContracts["treasuryVault"] + dai = deployedContracts["dai"] + sanctuary = deployedContracts['sanctuary'] + sdt = deployedContracts['sdt'] + + chain.sleep(7 * 86400) + treasuryVaultSDTBefore = sdt.balanceOf(treasuryVault.address) + sanctuarySDTBefore = sdt.balanceOf(sanctuary.address) + + # treasuryVault.toGovernance(sdt.address, treasuryVaultSDTBefore, multisig) + # sdt.transfer(sanctuary.address, treasuryVaultSDTBefore, multisig) + sanctuary.setRewardDistribution(treasuryVault.address, deployer) + treasuryVault.toGovernanceStaking(multisig) + + treasuryVaultSDTAfter = sdt.balanceOf(treasuryVault.address) + sanctuarySDTAfter = sdt.balanceOf(sanctuary.address) + print("SDT Sanctuary receives:", + (sanctuarySDTAfter - sanctuarySDTBefore) / 10**18) + assert treasuryVaultSDTBefore - \ + treasuryVaultSDTAfter == sanctuarySDTAfter - sanctuarySDTBefore + chain.snapshot() + + +def test_unstake_sdt(deployedContracts): + sdt = deployedContracts["sdt"] + sanctuary = deployedContracts['sanctuary'] + deployer = deployedContracts["deployer"] + SDT_STAKE_AMOUNT = deployedContracts["SDT_STAKE_AMOUNT"] + treasuryVault = deployedContracts["treasuryVault"] + multisig = deployedContracts["multisig"] + + user0xSDTBefore = sanctuary.balanceOf(deployer["from"]) + user0SDTBefore = sdt.balanceOf(deployer['from']) + user1xSDTBefore = sanctuary.balanceOf(accounts[1]) + user1SDTBefore = sdt.balanceOf(accounts[1]) + user2xSDTBefore = sanctuary.balanceOf(accounts[2]) + user2SDTBefore = sdt.balanceOf(accounts[2]) + sanctuaryxSDTBefore = sanctuary.totalSupply() + sanctuarySDTBefore = sdt.balanceOf(sanctuary.address) + print() + print("UNSTAKE") + print('user0xSDTBefore', user0xSDTBefore / 10**18) + print('user0SDTBefore', user0SDTBefore / 10**18) + print('user1xSDTBefore', user1xSDTBefore / 10**18) + print('user1SDTBefore', user1SDTBefore / 10**18) + print('user2xSDTBefore', user2xSDTBefore / 10**18) + print('user2SDTBefore', user2SDTBefore / 10**18) + print('sanctuaryxSDTBefore', sanctuaryxSDTBefore / 10**18) + print('sanctuarySDTBefore', sanctuarySDTBefore / 10**18) + print() + # sdt.transfer(sanctuary.address, 10**5 * 10**18, multisig) + sanctuary.leave(SDT_STAKE_AMOUNT, deployer) + sanctuary.leave(1000 * 10**18, {'from': accounts[1]}) + sanctuary.leave(1000000 * 10**18, {'from': accounts[2]}) + + user0xSDTAfter = sanctuary.balanceOf(deployer["from"]) + user0SDTAfter = sdt.balanceOf(deployer['from']) + user1xSDTAfter = sanctuary.balanceOf(accounts[1]) + user1SDTAfter = sdt.balanceOf(accounts[1]) + user2xSDTAfter = sanctuary.balanceOf(accounts[2]) + user2SDTAfter = sdt.balanceOf(accounts[2]) + sanctuaryxSDTAfter = sanctuary.totalSupply() + sanctuarySDTAfter = sdt.balanceOf(sanctuary.address) + print() + print('user0xSDTAfter', user0xSDTAfter / 10**18) + print('user0SDTAfter', user0SDTAfter / 10**18) + print('user1xSDTAfter', user1xSDTAfter / 10**18) + print('user1SDTAfter', user1SDTAfter / 10**18) + print('user2xSDTAfter', user2xSDTAfter / 10**18) + print('user2SDTAfter', user2SDTAfter / 10**18) + print('sanctuaryxSDTAfter', sanctuaryxSDTAfter / 10**18) + print('sanctuarySDTAfter', sanctuarySDTAfter / 10**18) + print() + + assert sanctuaryxSDTBefore - sanctuaryxSDTAfter == user0xSDTBefore - \ + user0xSDTAfter + user1xSDTBefore - \ + user1xSDTAfter + user2xSDTBefore - user2xSDTAfter + assert user0SDTAfter - user0SDTBefore + user1SDTAfter - user1SDTBefore + \ + user2SDTAfter - user2SDTBefore == sanctuarySDTBefore - sanctuarySDTAfter + + +def test_stake_sdt_on_nonempty_sanctuary(deployedContracts): + sdt = deployedContracts["sdt"] + sanctuary = deployedContracts['sanctuary'] + deployer = deployedContracts["deployer"] + SDT_STAKE_AMOUNT = deployedContracts["SDT_STAKE_AMOUNT"] + treasuryVault = deployedContracts["treasuryVault"] + multisig = deployedContracts["multisig"] + # depositing 100k SDT to sanctuary, making it non-empty + sdt.transfer(sanctuary.address, 10**5 * 10**18, multisig) + # sdt.approve(sanctuary.address, 10**9 * 10**18, multisig) + # sanctuary.enter(10**5 * 10**18, multisig) + + user0xSDTBefore = sanctuary.balanceOf(deployer["from"]) + user0SDTBefore = sdt.balanceOf(deployer['from']) + user1xSDTBefore = sanctuary.balanceOf(accounts[1]) + user1SDTBefore = sdt.balanceOf(accounts[1]) + user2xSDTBefore = sanctuary.balanceOf(accounts[2]) + user2SDTBefore = sdt.balanceOf(accounts[2]) + sanctuaryxSDTBefore = sanctuary.totalSupply() + sanctuarySDTBefore = sdt.balanceOf(sanctuary.address) + print() + print("STAKE ON NON-EMPTY SCENE #1") + print('user0xSDTBefore', user0xSDTBefore / 10**18) + print('user0SDTBefore', user0SDTBefore / 10**18) + print('user1xSDTBefore', user1xSDTBefore / 10**18) + print('user1SDTBefore', user1SDTBefore / 10**18) + print('user2xSDTBefore', user2xSDTBefore / 10**18) + print('user2SDTBefore', user2SDTBefore / 10**18) + print('sanctuaryxSDTBefore', sanctuaryxSDTBefore / 10**18) + print('sanctuarySDTBefore', sanctuarySDTBefore / 10**18) + print() + # approve and stake sdt + sdt.approve(sanctuary.address, 10**9 * 10**18, deployer) + sdt.approve(sanctuary.address, 10**9 * 10**18, {'from': accounts[1]}) + sdt.approve(sanctuary.address, 10**9 * 10**18, {'from': accounts[2]}) + sanctuary.enter(SDT_STAKE_AMOUNT, deployer) + sanctuary.enter(1000 * 10**18, {'from': accounts[1]}) + sanctuary.enter(1000000 * 10**18, {'from': accounts[2]}) + + user0xSDTAfter = sanctuary.balanceOf(deployer["from"]) + user0SDTAfter = sdt.balanceOf(deployer['from']) + user1xSDTAfter = sanctuary.balanceOf(accounts[1]) + user1SDTAfter = sdt.balanceOf(accounts[1]) + user2xSDTAfter = sanctuary.balanceOf(accounts[2]) + user2SDTAfter = sdt.balanceOf(accounts[2]) + sanctuaryxSDTAfter = sanctuary.totalSupply() + sanctuarySDTAfter = sdt.balanceOf(sanctuary.address) + print() + print('user0xSDTAfter', user0xSDTAfter / 10**18) + print('user0SDTAfter', user0SDTAfter / 10**18) + print('user1xSDTAfter', user1xSDTAfter / 10**18) + print('user1SDTAfter', user1SDTAfter / 10**18) + print('user2xSDTAfter', user2xSDTAfter / 10**18) + print('user2SDTAfter', user2SDTAfter / 10**18) + print('sanctuaryxSDTAfter', sanctuaryxSDTAfter / 10**18) + print('sanctuarySDTAfter', sanctuarySDTAfter / 10**18) + print() + + assert sanctuaryxSDTAfter - sanctuaryxSDTBefore == user0xSDTAfter - \ + user0xSDTBefore + user1xSDTAfter - \ + user1xSDTBefore + user2xSDTAfter - user2xSDTBefore + assert user0SDTBefore - user0SDTAfter + user1SDTBefore - user1SDTAfter + \ + user2SDTBefore - user2SDTAfter == sanctuarySDTAfter - sanctuarySDTBefore + + +def test_unstake_sdt_on_nonempty_sanctuary(deployedContracts): + # A receives 100k SDT as he was first to stake, after SDT fee deposit + sdt = deployedContracts["sdt"] + sanctuary = deployedContracts['sanctuary'] + deployer = deployedContracts["deployer"] + SDT_STAKE_AMOUNT = deployedContracts["SDT_STAKE_AMOUNT"] + treasuryVault = deployedContracts["treasuryVault"] + multisig = deployedContracts["multisig"] + + user0xSDTBefore = sanctuary.balanceOf(deployer["from"]) + user0SDTBefore = sdt.balanceOf(deployer['from']) + user1xSDTBefore = sanctuary.balanceOf(accounts[1]) + user1SDTBefore = sdt.balanceOf(accounts[1]) + user2xSDTBefore = sanctuary.balanceOf(accounts[2]) + user2SDTBefore = sdt.balanceOf(accounts[2]) + sanctuaryxSDTBefore = sanctuary.totalSupply() + sanctuarySDTBefore = sdt.balanceOf(sanctuary.address) + print() + print("UNSTAKE ON NON_EMPTY SCENE #1") + print('user0xSDTBefore', user0xSDTBefore / 10**18) + print('user0SDTBefore', user0SDTBefore / 10**18) + print('user1xSDTBefore', user1xSDTBefore / 10**18) + print('user1SDTBefore', user1SDTBefore / 10**18) + print('user2xSDTBefore', user2xSDTBefore / 10**18) + print('user2SDTBefore', user2SDTBefore / 10**18) + print('sanctuaryxSDTBefore', sanctuaryxSDTBefore / 10**18) + print('sanctuarySDTBefore', sanctuarySDTBefore / 10**18) + print() + # sdt.transfer(sanctuary.address, 10**5 * 10**18, multisig) + sanctuary.leave(user1xSDTBefore, {'from': accounts[1]}) + sanctuary.leave(user0xSDTBefore, deployer) + sanctuary.leave(user2xSDTBefore, {'from': accounts[2]}) + + user0xSDTAfter = sanctuary.balanceOf(deployer["from"]) + user0SDTAfter = sdt.balanceOf(deployer['from']) + user1xSDTAfter = sanctuary.balanceOf(accounts[1]) + user1SDTAfter = sdt.balanceOf(accounts[1]) + user2xSDTAfter = sanctuary.balanceOf(accounts[2]) + user2SDTAfter = sdt.balanceOf(accounts[2]) + sanctuaryxSDTAfter = sanctuary.totalSupply() + sanctuarySDTAfter = sdt.balanceOf(sanctuary.address) + print() + print('user0xSDTAfter', user0xSDTAfter / 10**18) + print('user0SDTAfter', user0SDTAfter / 10**18) + print('user1xSDTAfter', user1xSDTAfter / 10**18) + print('user1SDTAfter', user1SDTAfter / 10**18) + print('user2xSDTAfter', user2xSDTAfter / 10**18) + print('user2SDTAfter', user2SDTAfter / 10**18) + print("SDT 0 rakes", (user0SDTAfter - user0SDTBefore) / 10**18) + print("SDT 1 rakes", (user1SDTAfter - user1SDTBefore) / 10**18) + print("SDT 2 rakes", (user2SDTAfter - user2SDTBefore) / 10**18) + print('sanctuaryxSDTAfter', sanctuaryxSDTAfter / 10**18) + print('sanctuarySDTAfter', sanctuarySDTAfter / 10**18) + print() + + assert sanctuaryxSDTBefore - sanctuaryxSDTAfter == user0xSDTBefore - \ + user0xSDTAfter + user1xSDTBefore - \ + user1xSDTAfter + user2xSDTBefore - user2xSDTAfter + assert user0SDTAfter - user0SDTBefore + user1SDTAfter - user1SDTBefore + \ + user2SDTAfter - user2SDTBefore == sanctuarySDTBefore - sanctuarySDTAfter + # A makes 100100 SDT i.e. gets all SDT + assert user0SDTAfter - user0SDTBefore > 100000 * 10**18 + assert user1SDTAfter - user1SDTBefore > (1000 - 1) * 10**18 + assert user2SDTAfter - user2SDTBefore > (10**6 - 1) * 10**18 + + +def test_unstake_sdt_scene_2(deployedContracts): + # reverting to state on line 208 + # B, C unstaking 50% of their shares, to stay inside pool for next Fee deposit + chain.revert() + sdt = deployedContracts["sdt"] + sanctuary = deployedContracts['sanctuary'] + deployer = deployedContracts["deployer"] + SDT_STAKE_AMOUNT = deployedContracts["SDT_STAKE_AMOUNT"] + treasuryVault = deployedContracts["treasuryVault"] + multisig = deployedContracts["multisig"] + + user0xSDTBefore = sanctuary.balanceOf(deployer["from"]) + user0SDTBefore = sdt.balanceOf(deployer['from']) + user1xSDTBefore = sanctuary.balanceOf(accounts[1]) + user1SDTBefore = sdt.balanceOf(accounts[1]) + user2xSDTBefore = sanctuary.balanceOf(accounts[2]) + user2SDTBefore = sdt.balanceOf(accounts[2]) + sanctuaryxSDTBefore = sanctuary.totalSupply() + sanctuarySDTBefore = sdt.balanceOf(sanctuary.address) + print() + print("UNSTAKE SCENE #2") + print('user0xSDTBefore', user0xSDTBefore / 10**18) + print('user0SDTBefore', user0SDTBefore / 10**18) + print('user1xSDTBefore', user1xSDTBefore / 10**18) + print('user1SDTBefore', user1SDTBefore / 10**18) + print('user2xSDTBefore', user2xSDTBefore / 10**18) + print('user2SDTBefore', user2SDTBefore / 10**18) + print('sanctuaryxSDTBefore', sanctuaryxSDTBefore / 10**18) + print('sanctuarySDTBefore', sanctuarySDTBefore / 10**18) + print() + # sdt.transfer(sanctuary.address, 10**5 * 10**18, multisig) + sanctuary.leave(SDT_STAKE_AMOUNT, deployer) + sanctuary.leave((1000 / 2) * 10**18, {'from': accounts[1]}) + sanctuary.leave((1000000 / 2) * 10**18, {'from': accounts[2]}) + + user0xSDTAfter = sanctuary.balanceOf(deployer["from"]) + user0SDTAfter = sdt.balanceOf(deployer['from']) + user1xSDTAfter = sanctuary.balanceOf(accounts[1]) + user1SDTAfter = sdt.balanceOf(accounts[1]) + user2xSDTAfter = sanctuary.balanceOf(accounts[2]) + user2SDTAfter = sdt.balanceOf(accounts[2]) + sanctuaryxSDTAfter = sanctuary.totalSupply() + sanctuarySDTAfter = sdt.balanceOf(sanctuary.address) + print() + print('user0xSDTAfter', user0xSDTAfter / 10**18) + print('user0SDTAfter', user0SDTAfter / 10**18) + print('user1xSDTAfter', user1xSDTAfter / 10**18) + print('user1SDTAfter', user1SDTAfter / 10**18) + print('user2xSDTAfter', user2xSDTAfter / 10**18) + print('user2SDTAfter', user2SDTAfter / 10**18) + print('sanctuaryxSDTAfter', sanctuaryxSDTAfter / 10**18) + print('sanctuarySDTAfter', sanctuarySDTAfter / 10**18) + print() + + assert sanctuaryxSDTBefore - sanctuaryxSDTAfter == user0xSDTBefore - \ + user0xSDTAfter + user1xSDTBefore - \ + user1xSDTAfter + user2xSDTBefore - user2xSDTAfter + assert user0SDTAfter - user0SDTBefore + user1SDTAfter - user1SDTBefore + \ + user2SDTAfter - user2SDTBefore == sanctuarySDTBefore - sanctuarySDTAfter + + +def test_stake_sdt_on_nonempty_sanctuary_scene_2(deployedContracts): + # only A approves and stakes sdt + sdt = deployedContracts["sdt"] + sanctuary = deployedContracts['sanctuary'] + deployer = deployedContracts["deployer"] + SDT_STAKE_AMOUNT = deployedContracts["SDT_STAKE_AMOUNT"] + treasuryVault = deployedContracts["treasuryVault"] + multisig = deployedContracts["multisig"] + # depositing 100k SDT to sanctuary + sdt.transfer(sanctuary.address, 10**5 * 10**18, multisig) + # sdt.approve(sanctuary.address, 10**9 * 10**18, multisig) + # sanctuary.enter(10**5 * 10**18, multisig) + print("SDT Sanctuary receives:", 100000.0) + + user0xSDTBefore = sanctuary.balanceOf(deployer["from"]) + user0SDTBefore = sdt.balanceOf(deployer['from']) + user1xSDTBefore = sanctuary.balanceOf(accounts[1]) + user1SDTBefore = sdt.balanceOf(accounts[1]) + user2xSDTBefore = sanctuary.balanceOf(accounts[2]) + user2SDTBefore = sdt.balanceOf(accounts[2]) + sanctuaryxSDTBefore = sanctuary.totalSupply() + sanctuarySDTBefore = sdt.balanceOf(sanctuary.address) + print() + print("STAKE ON NON-EMPTY SCENE #2") + print('user0xSDTBefore', user0xSDTBefore / 10**18) + print('user0SDTBefore', user0SDTBefore / 10**18) + print('user1xSDTBefore', user1xSDTBefore / 10**18) + print('user1SDTBefore', user1SDTBefore / 10**18) + print('user2xSDTBefore', user2xSDTBefore / 10**18) + print('user2SDTBefore', user2SDTBefore / 10**18) + print('sanctuaryxSDTBefore', sanctuaryxSDTBefore / 10**18) + print('sanctuarySDTBefore', sanctuarySDTBefore / 10**18) + print() + # only A approves and stakes sdt + sdt.approve(sanctuary.address, 10**9 * 10**18, deployer) + # sdt.approve(sanctuary.address, 10**9 * 10**18, {'from': accounts[1]}) + # sdt.approve(sanctuary.address, 10**9 * 10**18, {'from': accounts[2]}) + sanctuary.enter(SDT_STAKE_AMOUNT, deployer) + # sanctuary.enter(1000 * 10**18, {'from': accounts[1]}) + # sanctuary.enter(1000000 * 10**18, {'from': accounts[2]}) + + user0xSDTAfter = sanctuary.balanceOf(deployer["from"]) + user0SDTAfter = sdt.balanceOf(deployer['from']) + user1xSDTAfter = sanctuary.balanceOf(accounts[1]) + user1SDTAfter = sdt.balanceOf(accounts[1]) + user2xSDTAfter = sanctuary.balanceOf(accounts[2]) + user2SDTAfter = sdt.balanceOf(accounts[2]) + sanctuaryxSDTAfter = sanctuary.totalSupply() + sanctuarySDTAfter = sdt.balanceOf(sanctuary.address) + print() + print('user0xSDTAfter', user0xSDTAfter / 10**18) + print('user0SDTAfter', user0SDTAfter / 10**18) + print('user1xSDTAfter', user1xSDTAfter / 10**18) + print('user1SDTAfter', user1SDTAfter / 10**18) + print('user2xSDTAfter', user2xSDTAfter / 10**18) + print('user2SDTAfter', user2SDTAfter / 10**18) + print('sanctuaryxSDTAfter', sanctuaryxSDTAfter / 10**18) + print('sanctuarySDTAfter', sanctuarySDTAfter / 10**18) + print() + + assert sanctuaryxSDTAfter - sanctuaryxSDTBefore == user0xSDTAfter - \ + user0xSDTBefore + assert user0SDTBefore - user0SDTAfter == sanctuarySDTAfter - sanctuarySDTBefore + + +def test_unstake_sdt_on_nonempty_sanctuary_scene_2(deployedContracts): + # A doesn't receive a share from 100k SDT, only B, C do as they stayed during the deposit + sdt = deployedContracts["sdt"] + sanctuary = deployedContracts['sanctuary'] + deployer = deployedContracts["deployer"] + SDT_STAKE_AMOUNT = deployedContracts["SDT_STAKE_AMOUNT"] + treasuryVault = deployedContracts["treasuryVault"] + multisig = deployedContracts["multisig"] + + user0xSDTBefore = sanctuary.balanceOf(deployer["from"]) + user0SDTBefore = sdt.balanceOf(deployer['from']) + user1xSDTBefore = sanctuary.balanceOf(accounts[1]) + user1SDTBefore = sdt.balanceOf(accounts[1]) + user2xSDTBefore = sanctuary.balanceOf(accounts[2]) + user2SDTBefore = sdt.balanceOf(accounts[2]) + sanctuaryxSDTBefore = sanctuary.totalSupply() + sanctuarySDTBefore = sdt.balanceOf(sanctuary.address) + print() + print("UNSTAKE ON NON_EMPTY SCENE #2") + print('user0xSDTBefore', user0xSDTBefore / 10**18) + print('user0SDTBefore', user0SDTBefore / 10**18) + print('user1xSDTBefore', user1xSDTBefore / 10**18) + print('user1SDTBefore', user1SDTBefore / 10**18) + print('user2xSDTBefore', user2xSDTBefore / 10**18) + print('user2SDTBefore', user2SDTBefore / 10**18) + print('sanctuaryxSDTBefore', sanctuaryxSDTBefore / 10**18) + print('sanctuarySDTBefore', sanctuarySDTBefore / 10**18) + print() + # sdt.transfer(sanctuary.address, 10**5 * 10**18, multisig) + sanctuary.leave(user1xSDTBefore, {'from': accounts[1]}) + sanctuary.leave(user0xSDTBefore, deployer) + sanctuary.leave(user2xSDTBefore, {'from': accounts[2]}) + + user0xSDTAfter = sanctuary.balanceOf(deployer["from"]) + user0SDTAfter = sdt.balanceOf(deployer['from']) + user1xSDTAfter = sanctuary.balanceOf(accounts[1]) + user1SDTAfter = sdt.balanceOf(accounts[1]) + user2xSDTAfter = sanctuary.balanceOf(accounts[2]) + user2SDTAfter = sdt.balanceOf(accounts[2]) + sanctuaryxSDTAfter = sanctuary.totalSupply() + sanctuarySDTAfter = sdt.balanceOf(sanctuary.address) + print() + print('user0xSDTAfter', user0xSDTAfter / 10**18) + print('user0SDTAfter', user0SDTAfter / 10**18) + print('user1xSDTAfter', user1xSDTAfter / 10**18) + print('user1SDTAfter', user1SDTAfter / 10**18) + print('user2xSDTAfter', user2xSDTAfter / 10**18) + print('user2SDTAfter', user2SDTAfter / 10**18) + print("SDT 0 rakes", (user0SDTAfter - user0SDTBefore) / 10**18) + print("SDT 1 rakes", (user1SDTAfter - user1SDTBefore) / 10**18) + print("SDT 2 rakes", (user2SDTAfter - user2SDTBefore) / 10**18) + print('sanctuaryxSDTAfter', sanctuaryxSDTAfter / 10**18) + print('sanctuarySDTAfter', sanctuarySDTAfter / 10**18) + print() + + assert sanctuaryxSDTBefore - sanctuaryxSDTAfter == user0xSDTBefore - \ + user0xSDTAfter + user1xSDTBefore - \ + user1xSDTAfter + user2xSDTBefore - user2xSDTAfter + assert user0SDTAfter - user0SDTBefore + user1SDTAfter - user1SDTBefore + \ + user2SDTAfter - user2SDTBefore == sanctuarySDTBefore - sanctuarySDTAfter + # A receiving max 100 SDT (their original deposit) on claiming + assert user0SDTAfter - user0SDTBefore <= 100 * 10**18 + assert user1SDTAfter - user1SDTBefore > 0 + assert user2SDTAfter - user2SDTBefore > 0 + + +def test_xSDT_fungibility(deployedContracts): + sdt = deployedContracts["sdt"] + sanctuary = deployedContracts['sanctuary'] + deployer = deployedContracts["deployer"] + SDT_STAKE_AMOUNT = deployedContracts["SDT_STAKE_AMOUNT"] + treasuryVault = deployedContracts["treasuryVault"] + + # approve and stake sdt + sdt.approve(sanctuary.address, 10**9 * 10**18, {'from': accounts[3]}) + sanctuary.enter(1000000 * 10**18, {'from': accounts[3]}) + + user3xSDTBefore = sanctuary.balanceOf(accounts[3]) + user2xSDTBefore = sanctuary.balanceOf(accounts[2]) + print() + print("TRANSFER") + print('user3xSDTBefore', user3xSDTBefore / 10**18) + print('user2xSDTBefore', user2xSDTBefore / 10**18) + print() + + sanctuary.transfer(accounts[2], 300000 * 10**18, {'from': accounts[3]}) + + user3xSDTAfter = sanctuary.balanceOf(accounts[3]) + user2xSDTAfter = sanctuary.balanceOf(accounts[2]) + print() + print('user3xSDTAfter', user3xSDTAfter / 10**18) + print('user2xSDTAfter', user2xSDTAfter / 10**18) + print() + + assert user3xSDTBefore - user3xSDTAfter == user2xSDTAfter - user2xSDTBefore diff --git a/protocol/tests/xToken/test_xToken.py b/protocol/tests/xToken/test_xToken.py new file mode 100644 index 0000000..fd74198 --- /dev/null +++ b/protocol/tests/xToken/test_xToken.py @@ -0,0 +1,248 @@ +import pytest +from brownie import * +import brownie +import typer +import json + + +@pytest.fixture(scope="module", autouse=True) +def deployedContracts(): + + deployed = open("config.json", "r") + + try: + deployed = json.loads(deployed.read()) + except: + deployed = {} + + ENV = "" + if (network.chain.id == 1): + ENV = "prod" + else: + ENV = "dev" + + # account used for executing transactions + DEFAULT_DEPLOYER_ACCOUNT = accounts[0] + + GNOSIS_SAFE_PROXY = deployed[ENV]['GNOSIS_SAFE_PROXY'] + + TREASURY_VAULT = deployed[ENV]['TREASURY_VAULT'] + treasuryVault = Contract(TREASURY_VAULT) + + VESTING_ESCROW = deployed[ENV]['VESTING_ESCROW'] + + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + deployer = {'from': DEFAULT_DEPLOYER_ACCOUNT} + + multisig = {'from': GNOSIS_SAFE_PROXY} + + controller = Controller.at(deployed[ENV]['CONTROLLER']) + + user = {'from': '0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8'} + + wrapper = xTokenWrapper.deploy(GNOSIS_SAFE_PROXY, deployer) + + dai = Contract('0x6b175474e89094c44da98b954eedeac495271d0f') + aave = Contract(wrapper.aave()) + + xAAVEa = Contract(wrapper.xAAVEa()) + + return {"aave": aave, "treasuryVault": treasuryVault, "multisig": multisig, "deployer": deployer, "wrapper": wrapper, "user": user, "controller": controller, "GNOSIS_SAFE_PROXY": GNOSIS_SAFE_PROXY, "xAAVEa": xAAVEa} + + +def test_xToken_mintWithToken(deployedContracts): + aave = deployedContracts['aave'] + wrapper = deployedContracts['wrapper'] + user = deployedContracts['user'] + GNOSIS_SAFE_PROXY = deployedContracts['GNOSIS_SAFE_PROXY'] + xAAVEa = deployedContracts['xAAVEa'] + + userAaveBefore = aave.balanceOf(user['from']) + userSdxAaveBefore = wrapper.balanceOf(user['from']) + wrapperAaveBefore = aave.balanceOf(wrapper.address) + wrapperXaaveBefore = xAAVEa.balanceOf(wrapper.address) + xAaveAaveBefore = aave.balanceOf(wrapper.xAAVEa()) + multisigAaveBefore = aave.balanceOf(GNOSIS_SAFE_PROXY) + + aave.approve(wrapper.address, 10**9 * 10**18, user) + wrapper.mintWithToken(500 * 10**18, user) + + userAaveAfter = aave.balanceOf(user['from']) + userSdxAaveAfter = wrapper.balanceOf(user['from']) + wrapperAaveAfter = aave.balanceOf(wrapper.address) + wrapperXaaveAfter = xAAVEa.balanceOf(wrapper.address) + xAaveAaveAfter = aave.balanceOf(wrapper.xAAVEa()) + multisigAaveAfter = aave.balanceOf(GNOSIS_SAFE_PROXY) + + assert userAaveBefore - userAaveAfter > 0 + assert multisigAaveAfter > multisigAaveBefore + # assert userAaveBefore - userAaveAfter == xAaveAaveAfter - \ + # xAaveAaveBefore + multisigAaveAfter - multisigAaveBefore + assert xAaveAaveAfter - xAaveAaveBefore > 0 + assert wrapperXaaveAfter - wrapperXaaveBefore > 0 + assert wrapperXaaveAfter - wrapperXaaveBefore == userSdxAaveAfter - userSdxAaveBefore + assert wrapperAaveAfter == wrapperAaveBefore == 0 + + chain.sleep(30 * 86400) + chain.mine(1000) + xAAVEa.claim({'from': '0x38138586aedb29b436eab16105b09c317f5a79dd'}) + chain.snapshot() + + +def test_xToken_burn_with_eth(deployedContracts): + aave = deployedContracts['aave'] + wrapper = deployedContracts['wrapper'] + user = deployedContracts['user'] + GNOSIS_SAFE_PROXY = deployedContracts['GNOSIS_SAFE_PROXY'] + xAAVEa = deployedContracts['xAAVEa'] + + userSdxAaveBefore = wrapper.balanceOf(user['from']) + wrapperAaveBefore = aave.balanceOf(wrapper.address) + wrapperXaaveBefore = xAAVEa.balanceOf(wrapper.address) + xAaveAaveBefore = aave.balanceOf(wrapper.xAAVEa()) + multisigAaveBefore = aave.balanceOf(GNOSIS_SAFE_PROXY) + + acc = accounts.at(user['from']) + before = acc.balance() + wrapper.burn(wrapper.balanceOf(user['from']), True, 0, user) + after = acc.balance() + + userSdxAaveAfter = wrapper.balanceOf(user['from']) + wrapperAaveAfter = aave.balanceOf(wrapper.address) + wrapperXaaveAfter = xAAVEa.balanceOf(wrapper.address) + xAaveAaveAfter = aave.balanceOf(wrapper.xAAVEa()) + multisigAaveAfter = aave.balanceOf(GNOSIS_SAFE_PROXY) + + print("Eth received", (after - before) / 10**18) + + assert after - before > 0 + assert wrapperXaaveBefore > wrapperXaaveAfter + assert wrapperXaaveBefore - wrapperXaaveAfter == userSdxAaveBefore - userSdxAaveAfter + assert wrapperAaveAfter == wrapperAaveBefore == 0 + assert multisigAaveAfter == multisigAaveBefore + + +def test_xToken_burn_with_aave(deployedContracts): + chain.revert() + aave = deployedContracts['aave'] + wrapper = deployedContracts['wrapper'] + user = deployedContracts['user'] + GNOSIS_SAFE_PROXY = deployedContracts['GNOSIS_SAFE_PROXY'] + xAAVEa = deployedContracts['xAAVEa'] + + userAaveBefore = aave.balanceOf(user['from']) + userSdxAaveBefore = wrapper.balanceOf(user['from']) + wrapperAaveBefore = aave.balanceOf(wrapper.address) + wrapperXaaveBefore = xAAVEa.balanceOf(wrapper.address) + xAaveAaveBefore = aave.balanceOf(wrapper.xAAVEa()) + multisigAaveBefore = aave.balanceOf(GNOSIS_SAFE_PROXY) + + wrapper.burn(wrapper.balanceOf(user['from']), False, 0, user) + + userAaveAfter = aave.balanceOf(user['from']) + userSdxAaveAfter = wrapper.balanceOf(user['from']) + wrapperAaveAfter = aave.balanceOf(wrapper.address) + wrapperXaaveAfter = xAAVEa.balanceOf(wrapper.address) + xAaveAaveAfter = aave.balanceOf(wrapper.xAAVEa()) + multisigAaveAfter = aave.balanceOf(GNOSIS_SAFE_PROXY) + + print("Aave received", (userAaveAfter - userAaveBefore) / 10**18) + + assert userAaveAfter - userAaveBefore > 0 + assert wrapperXaaveBefore > wrapperXaaveAfter + assert wrapperXaaveBefore - wrapperXaaveAfter == userSdxAaveBefore - userSdxAaveAfter + assert wrapperAaveAfter == wrapperAaveBefore == 0 + assert multisigAaveAfter == multisigAaveBefore + + +def test_xToken_redeem_sdxAavea_for_xAavea(deployedContracts): + chain.revert() + aave = deployedContracts['aave'] + wrapper = deployedContracts['wrapper'] + user = deployedContracts['user'] + GNOSIS_SAFE_PROXY = deployedContracts['GNOSIS_SAFE_PROXY'] + xAAVEa = deployedContracts['xAAVEa'] + + userxAaveBefore = xAAVEa.balanceOf(user['from']) + userSdxAaveBefore = wrapper.balanceOf(user['from']) + wrapperAaveBefore = aave.balanceOf(wrapper.address) + wrapperXaaveBefore = xAAVEa.balanceOf(wrapper.address) + + wrapper.redeemsdxAAVEeaForxAAVEa(wrapper.balanceOf(user['from']), user) + + userxAaveAfter = xAAVEa.balanceOf(user['from']) + userSdxAaveAfter = wrapper.balanceOf(user['from']) + wrapperAaveAfter = aave.balanceOf(wrapper.address) + wrapperXaaveAfter = xAAVEa.balanceOf(wrapper.address) + + print("xAave received", (userxAaveAfter - userxAaveBefore) / 10**18) + + assert userxAaveAfter - userxAaveBefore > 0 + assert userxAaveAfter - userxAaveBefore == wrapperXaaveBefore - wrapperXaaveAfter + assert userSdxAaveBefore - userSdxAaveAfter == wrapperXaaveBefore - wrapperXaaveAfter + assert wrapperAaveAfter == wrapperAaveBefore == 0 + + +def test_xToken_whale_mint_burn_fail(deployedContracts): + aave = deployedContracts['aave'] + wrapper = deployedContracts['wrapper'] + user = deployedContracts['user'] + GNOSIS_SAFE_PROXY = deployedContracts['GNOSIS_SAFE_PROXY'] + xAAVEa = deployedContracts['xAAVEa'] + + AAVE_WHALE = '0x26a78d5b6d7a7aceedd1e6ee3229b372a624d8b7' + aave_whale = {'from': AAVE_WHALE} + + # mint + userAaveBefore = aave.balanceOf(AAVE_WHALE) + userSdxAaveBefore = wrapper.balanceOf(AAVE_WHALE) + wrapperAaveBefore = aave.balanceOf(wrapper.address) + wrapperXaaveBefore = xAAVEa.balanceOf(wrapper.address) + xAaveAaveBefore = aave.balanceOf(wrapper.xAAVEa()) + multisigAaveBefore = aave.balanceOf(GNOSIS_SAFE_PROXY) + + aave.approve(wrapper.address, 10**9 * 10**18, aave_whale) + wrapper.mintWithToken(100000 * 10**18, aave_whale) + + userAaveAfter = aave.balanceOf(AAVE_WHALE) + userSdxAaveAfter = wrapper.balanceOf(AAVE_WHALE) + wrapperAaveAfter = aave.balanceOf(wrapper.address) + wrapperXaaveAfter = xAAVEa.balanceOf(wrapper.address) + xAaveAaveAfter = aave.balanceOf(wrapper.xAAVEa()) + multisigAaveAfter = aave.balanceOf(GNOSIS_SAFE_PROXY) + + assert userAaveBefore - userAaveAfter > 0 + assert multisigAaveAfter > multisigAaveBefore + assert xAaveAaveAfter - xAaveAaveBefore > 0 + assert wrapperXaaveAfter - wrapperXaaveBefore > 0 + assert wrapperXaaveAfter - wrapperXaaveBefore == userSdxAaveAfter - userSdxAaveBefore + assert wrapperAaveAfter == wrapperAaveBefore == 0 + + chain.sleep(30 * 86400) + chain.mine(1000) + xAAVEa.claim({'from': '0x38138586aedb29b436eab16105b09c317f5a79dd'}) + + # burn fail + userAaveBefore = aave.balanceOf(AAVE_WHALE) + userSdxAaveBefore = wrapper.balanceOf(AAVE_WHALE) + wrapperAaveBefore = aave.balanceOf(wrapper.address) + wrapperXaaveBefore = xAAVEa.balanceOf(wrapper.address) + + with brownie.reverts("Insufficient exit liquidity"): + wrapper.burn(wrapper.balanceOf(AAVE_WHALE), False, 0, aave_whale) + + +def test_xToken_whale_redeem_fail(deployedContracts): + aave = deployedContracts['aave'] + wrapper = deployedContracts['wrapper'] + user = deployedContracts['user'] + GNOSIS_SAFE_PROXY = deployedContracts['GNOSIS_SAFE_PROXY'] + xAAVEa = deployedContracts['xAAVEa'] + + AAVE_WHALE = '0x26a78d5b6d7a7aceedd1e6ee3229b372a624d8b7' + aave_whale = {'from': AAVE_WHALE} + + with brownie.reverts("ERC20: burn amount exceeds balance"): + wrapper.redeemsdxAAVEeaForxAAVEa( + wrapper.balanceOf(AAVE_WHALE) + 1, aave_whale)