diff --git a/contracts/v3/alchemix/AlToken.sol b/contracts/v3/alchemix/AlToken.sol new file mode 100644 index 00000000..e109619c --- /dev/null +++ b/contracts/v3/alchemix/AlToken.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +import {IDetailedERC20} from './interfaces/IDetailedERC20.sol'; + +/// @title AlToken +/// +/// @dev This is the contract for the Yaxis utillity token usd. +/// +/// Initially, the contract deployer is given both the admin and minter role. This allows them to pre-mine tokens, +/// transfer admin to a timelock contract, and lastly, grant the staking pools the minter role. After this is done, +/// the deployer must revoke their admin role and minter role. +contract AlToken is AccessControl, ERC20('Yaxis USD', 'yalUSD') { + using SafeERC20 for ERC20; + + /// @dev The identifier of the role which maintains other roles. + bytes32 public constant ADMIN_ROLE = keccak256('ADMIN'); + + /// @dev The identifier of the role which allows accounts to mint tokens. + bytes32 public constant SENTINEL_ROLE = keccak256('SENTINEL'); + + /// @dev addresses whitelisted for minting new tokens + mapping(address => bool) public whiteList; + + /// @dev addresses blacklisted for minting new tokens + mapping(address => bool) public blacklist; + + /// @dev addresses paused for minting new tokens + mapping(address => bool) public paused; + + /// @dev ceiling per address for minting new tokens + mapping(address => uint256) public ceiling; + + /// @dev already minted amount per address to track the ceiling + mapping(address => uint256) public hasMinted; + + event Paused(address alchemistAddress, bool isPaused); + + constructor() public { + _setupRole(ADMIN_ROLE, msg.sender); + _setupRole(SENTINEL_ROLE, msg.sender); + _setRoleAdmin(SENTINEL_ROLE, ADMIN_ROLE); + _setRoleAdmin(ADMIN_ROLE, ADMIN_ROLE); + } + + /// @dev A modifier which checks if whitelisted for minting. + modifier onlyWhitelisted() { + require(whiteList[msg.sender], 'AlUSD: Alchemist is not whitelisted'); + _; + } + + /// @dev Mints tokens to a recipient. + /// + /// This function reverts if the caller does not have the minter role. + /// + /// @param _recipient the account to mint tokens to. + /// @param _amount the amount of tokens to mint. + function mint(address _recipient, uint256 _amount) external onlyWhitelisted { + require(!blacklist[msg.sender], 'AlUSD: Alchemist is blacklisted.'); + require(!paused[msg.sender], 'AlUSD: user is currently paused.'); + uint256 _total = _amount.add(hasMinted[msg.sender]); + require(_total <= ceiling[msg.sender], "AlUSD: Alchemist's ceiling was breached."); + hasMinted[msg.sender] = _total; + _mint(_recipient, _amount); + } + + /// This function reverts if the caller does not have the admin role. + /// + /// @param _toWhitelist the account to mint tokens to. + /// @param _state the whitelist state. + + function setWhitelist(address _toWhitelist, bool _state) external onlyAdmin { + whiteList[_toWhitelist] = _state; + } + + /// This function reverts if the caller does not have the admin role. + /// + /// @param _newSentinel the account to set as sentinel. + + function setSentinel(address _newSentinel) external onlyAdmin { + _setupRole(SENTINEL_ROLE, _newSentinel); + } + + /// This function reverts if the caller does not have the sentinel role. + /// + /// @param _toBlacklist the account to mint tokens to. + function setBlacklist(address _toBlacklist) external onlySentinel { + blacklist[_toBlacklist] = true; + } + + /// This function reverts if the caller does not have the sentinel role. + function pauseAlchemist(address _toPause, bool _state) external onlySentinel { + paused[_toPause] = _state; + emit Paused(_toPause, _state); + } + + /// This function reverts if the caller does not have the admin role. + /// + /// @param _toSetCeiling the account set the ceiling off. + /// @param _ceiling the max amount of tokens the account is allowed to mint. + function setCeiling(address _toSetCeiling, uint256 _ceiling) external onlyAdmin { + ceiling[_toSetCeiling] = _ceiling; + } + + /// @dev A modifier which checks that the caller has the admin role. + modifier onlyAdmin() { + require(hasRole(ADMIN_ROLE, msg.sender), 'only admin'); + _; + } + /// @dev A modifier which checks that the caller has the sentinel role. + modifier onlySentinel() { + require(hasRole(SENTINEL_ROLE, msg.sender), 'only sentinel'); + _; + } + + /** + * @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); + } + + /** + * @dev lowers hasminted from the caller's allocation + * + */ + function lowerHasMinted(uint256 amount) public onlyWhitelisted { + hasMinted[msg.sender] = hasMinted[msg.sender].sub(amount); + } +} diff --git a/contracts/v3/alchemix/Alchemist.sol b/contracts/v3/alchemix/Alchemist.sol new file mode 100644 index 00000000..20322ce6 --- /dev/null +++ b/contracts/v3/alchemix/Alchemist.sol @@ -0,0 +1,853 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +//import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Math} from '@openzeppelin/contracts/math/Math.sol'; +import {ReentrancyGuard} from '@openzeppelin/contracts/utils/ReentrancyGuard.sol'; +import {Address} from '@openzeppelin/contracts/utils/Address.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol'; + +import {CDP} from './libraries/alchemist/CDP.sol'; +import {FixedPointMath} from './libraries/FixedPointMath.sol'; +import {AlchemistVault} from './libraries/alchemist/AlchemistVault.sol'; +import {ITransmuter} from './interfaces/ITransmuter.sol'; +import {IMintableERC20} from './interfaces/IMintableERC20.sol'; +import {ICurveToken} from './interfaces/ICurveToken.sol'; +import {IVaultAdapter} from './interfaces/IVaultAdapter.sol'; + +import 'hardhat/console.sol'; + +// ERC20,removing ERC20 from the alchemist +// ___ __ __ _ ___ __ _ +// / _ | / / ____ / / ___ __ _ (_) __ __ / _ \ ____ ___ ___ ___ ___ / /_ ___ (_) +// / __ | / / / __/ / _ \/ -_) / ' \ / / \ \ / / ___/ / __// -_) (_- CDP.Data) private _cdps; + + /// @dev A list of all of the vaults. The last element of the list is the vault that is currently being used for + /// deposits and withdraws. Vaults before the last element are considered inactive and are expected to be cleared. + AlchemistVault.List private _vaults; + + /// @dev The minimum returned amount needed to be on peg according to the oracle. + uint256 public pegMinimum; + + constructor( + IMintableERC20 _token, + IMintableERC20 _xtoken, + address _governance, + address _sentinel + ) + public + /*ERC20( + string(abi.encodePacked("Alchemic ", _token.name())), + string(abi.encodePacked("al", _token.symbol())) + )*/ + { + require(_governance != ZERO_ADDRESS, 'Alchemist: governance address cannot be 0x0.'); + require(_sentinel != ZERO_ADDRESS, 'Alchemist: sentinel address cannot be 0x0.'); + + token = _token; + xtoken = _xtoken; + governance = _governance; + sentinel = _sentinel; + flushActivator = 100000 ether; // change for non 18 digit tokens + + //_setupDecimals(_token.decimals()); + uint256 COLL_LIMIT = MINIMUM_COLLATERALIZATION_LIMIT.mul(2); + _ctx.collateralizationLimit = FixedPointMath.FixedDecimal(COLL_LIMIT); + _ctx.accumulatedYieldWeight = FixedPointMath.FixedDecimal(0); + } + + /// @dev Sets the pending governance. + /// + /// This function reverts if the new pending governance is the zero address or the caller is not the current + /// governance. This is to prevent the contract governance being set to the zero address which would deadlock + /// privileged contract functionality. + /// + /// @param _pendingGovernance the new pending governance. + function setPendingGovernance(address _pendingGovernance) external onlyGov { + require( + _pendingGovernance != ZERO_ADDRESS, + 'Alchemist: governance address cannot be 0x0.' + ); + + pendingGovernance = _pendingGovernance; + + emit PendingGovernanceUpdated(_pendingGovernance); + } + + /// @dev Accepts the role as governance. + /// + /// This function reverts if the caller is not the new pending governance. + function acceptGovernance() external { + address _pendingGovernance = pendingGovernance; + require(msg.sender == _pendingGovernance, 'sender is not pendingGovernance'); + governance = _pendingGovernance; + + emit GovernanceUpdated(_pendingGovernance); + } + + function setSentinel(address _sentinel) external onlyGov { + require(_sentinel != ZERO_ADDRESS, 'Alchemist: sentinel address cannot be 0x0.'); + + sentinel = _sentinel; + + emit SentinelUpdated(_sentinel); + } + + /// @dev Sets the transmuter. + /// + /// This function reverts if the new transmuter is the zero address or the caller is not the current governance. + /// + /// @param _transmuter the new transmuter. + function setTransmuter(address _transmuter) external onlyGov { + // Check that the transmuter address is not the zero address. Setting the transmuter to the zero address would break + // transfers to the address because of `safeTransfer` checks. + require(_transmuter != ZERO_ADDRESS, 'Alchemist: transmuter address cannot be 0x0.'); + + transmuter = _transmuter; + + emit TransmuterUpdated(_transmuter); + } + + /// @dev Sets the flushActivator. + /// + /// @param _flushActivator the new flushActivator. + function setFlushActivator(uint256 _flushActivator) external onlyGov { + flushActivator = _flushActivator; + } + + /// @dev Sets the rewards contract. + /// + /// This function reverts if the new rewards contract is the zero address or the caller is not the current governance. + /// + /// @param _rewards the new rewards contract. + function setRewards(address _rewards) external onlyGov { + // Check that the rewards address is not the zero address. Setting the rewards to the zero address would break + // transfers to the address because of `safeTransfer` checks. + require(_rewards != ZERO_ADDRESS, 'Alchemist: rewards address cannot be 0x0.'); + + rewards = _rewards; + + emit RewardsUpdated(_rewards); + } + + /// @dev Sets the harvest fee. + /// + /// This function reverts if the caller is not the current governance. + /// + /// @param _harvestFee the new harvest fee. + function setHarvestFee(uint256 _harvestFee) external onlyGov { + // Check that the harvest fee is within the acceptable range. Setting the harvest fee greater than 100% could + // potentially break internal logic when calculating the harvest fee. + require(_harvestFee <= PERCENT_RESOLUTION, 'Alchemist: harvest fee above maximum.'); + + harvestFee = _harvestFee; + + emit HarvestFeeUpdated(_harvestFee); + } + + /// @dev Sets the borrow fee. + /// + /// This function reverts if the caller is not the current governance. + /// + /// @param _borrowFee the new borrow fee. + function setBorrowFee(uint256 _borrowFee) external onlyGov { + // Check that the borrow fee is within the acceptable range. Setting the borrow fee greater than 100% could + // potentially break internal logic when calculating the borrow fee. + require(_borrowFee <= PERCENT_RESOLUTION, 'Alchemist: borrow fee above maximum.'); + + borrowFee = _borrowFee; + + emit BorrowFeeUpdated(_borrowFee); + } + + /// @dev Sets the collateralization limit. + /// + /// This function reverts if the caller is not the current governance or if the collateralization limit is outside + /// of the accepted bounds. + /// + /// @param _limit the new collateralization limit. + function setCollateralizationLimit(uint256 _limit) external onlyGov { + require( + _limit >= MINIMUM_COLLATERALIZATION_LIMIT, + 'Alchemist: collateralization limit below minimum.' + ); + require( + _limit <= MAXIMUM_COLLATERALIZATION_LIMIT, + 'Alchemist: collateralization limit above maximum.' + ); + + _ctx.collateralizationLimit = FixedPointMath.FixedDecimal(_limit); + + emit CollateralizationLimitUpdated(_limit); + } + + /// @dev Set pegMinimum. + function setPegMinimum(uint256 peg) external onlyGov { + pegMinimum = peg; + emit PegMinimumUpdated(pegMinimum); + } + + /// @dev Sets if the contract should enter emergency exit mode. + /// + /// @param _emergencyExit if the contract should enter emergency exit mode. + function setEmergencyExit(bool _emergencyExit) external { + require(msg.sender == governance || msg.sender == sentinel, ''); + + emergencyExit = _emergencyExit; + + emit EmergencyExitUpdated(_emergencyExit); + } + + /// @dev Gets the collateralization limit. + /// + /// The collateralization limit is the minimum ratio of collateral to debt that is allowed by the system. + /// + /// @return the collateralization limit. + function collateralizationLimit() + external + view + returns (FixedPointMath.FixedDecimal memory) + { + return _ctx.collateralizationLimit; + } + + /// @dev Initializes the contract. + /// + /// This function checks that the transmuter and rewards have been set and sets up the active vault. + /// + /// @param _adapter the vault adapter of the active vault. + function initialize(IVaultAdapter _adapter) external onlyGov { + require(!initialized, 'Alchemist: already initialized'); + + require( + transmuter != ZERO_ADDRESS, + 'Alchemist: cannot initialize transmuter address to 0x0' + ); + require( + rewards != ZERO_ADDRESS, + 'Alchemist: cannot initialize rewards address to 0x0' + ); + + _updateActiveVault(_adapter); + + initialized = true; + } + + /// @dev Migrates the system to a new vault. + /// + /// This function reverts if the vault adapter is the zero address, if the token that the vault adapter accepts + /// is not the token that this contract defines as the parent asset, or if the contract has not yet been initialized. + /// + /// @param _adapter the adapter for the vault the system will migrate to. + function migrate(IVaultAdapter _adapter) external expectInitialized onlyGov { + _updateActiveVault(_adapter); + } + + /// @dev Harvests yield from a vault. + /// + /// @param _vaultId the identifier of the vault to harvest from. + /// + /// @return the amount of funds that were harvested from the vault. + function harvest(uint256 _vaultId) external expectInitialized returns (uint256, uint256) { + AlchemistVault.Data storage _vault = _vaults.get(_vaultId); + + (uint256 _harvestedAmount, uint256 _decreasedValue) = _vault.harvest(address(this)); + + if (_harvestedAmount > 0) { + uint256 _feeAmount = _harvestedAmount.mul(harvestFee).div(PERCENT_RESOLUTION); + uint256 _distributeAmount = _harvestedAmount.sub(_feeAmount); + + FixedPointMath.FixedDecimal memory _weight = FixedPointMath + .fromU256(_distributeAmount) + .div(totalDeposited); + _ctx.accumulatedYieldWeight = _ctx.accumulatedYieldWeight.add(_weight); + + if (_feeAmount > 0) { + token.safeTransfer(rewards, _feeAmount); + } + + if (_distributeAmount > 0) { + _distributeToTransmuter(_distributeAmount); + + // token.safeTransfer(transmuter, _distributeAmount); previous version call + } + } + + emit FundsHarvested(_harvestedAmount, _decreasedValue); + + return (_harvestedAmount, _decreasedValue); + } + + /// @dev Recalls an amount of deposited funds from a vault to this contract. + /// + /// @param _vaultId the identifier of the recall funds from. + /// + /// @return the amount of funds that were recalled from the vault to this contract and the decreased vault value. + function recall(uint256 _vaultId, uint256 _amount) + external + nonReentrant + expectInitialized + returns (uint256, uint256) + { + return _recallFunds(_vaultId, _amount); + } + + /// @dev Recalls all the deposited funds from a vault to this contract. + /// + /// @param _vaultId the identifier of the recall funds from. + /// + /// @return the amount of funds that were recalled from the vault to this contract and the decreased vault value. + function recallAll(uint256 _vaultId) + external + nonReentrant + expectInitialized + returns (uint256, uint256) + { + AlchemistVault.Data storage _vault = _vaults.get(_vaultId); + return _recallFunds(_vaultId, _vault.totalDeposited); + } + + /// @dev Flushes buffered tokens to the active vault. + /// + /// This function reverts if an emergency exit is active. This is in place to prevent the potential loss of + /// additional funds. + /// + /// @return the amount of tokens flushed to the active vault. + function flush() external nonReentrant notEmergency expectInitialized returns (uint256) { + // Prevent flushing to the active vault when an emergency exit is enabled to prevent potential loss of funds if + // the active vault is poisoned for any reason. + return flushActiveVault(); + } + + /// @dev Internal function to flush buffered tokens to the active vault. + /// + /// This function reverts if an emergency exit is active. This is in place to prevent the potential loss of + /// additional funds. + /// + /// @return the amount of tokens flushed to the active vault. + function flushActiveVault() internal returns (uint256) { + AlchemistVault.Data storage _activeVault = _vaults.last(); + uint256 _depositedAmount = _activeVault.depositAll(); + + emit FundsFlushed(_depositedAmount); + + return _depositedAmount; + } + + /// @dev Deposits collateral into a CDP. + /// + /// This function reverts if an emergency exit is active. This is in place to prevent the potential loss of + /// additional funds. + /// + /// @param _amount the amount of collateral to deposit. + function deposit(uint256 _amount) + external + nonReentrant + notEmergency + noContractAllowed + expectInitialized + { + CDP.Data storage _cdp = _cdps[msg.sender]; + _cdp.update(_ctx); + + token.safeTransferFrom(msg.sender, address(this), _amount); + if (_amount >= flushActivator) { + flushActiveVault(); + } + totalDeposited = totalDeposited.add(_amount); + + _cdp.totalDeposited = _cdp.totalDeposited.add(_amount); + _cdp.lastDeposit = block.number; + + emit TokensDeposited(msg.sender, _amount); + } + + /// @dev Attempts to withdraw part of a CDP's collateral. + /// + /// This function reverts if a deposit into the CDP was made in the same block. This is to prevent flash loan attacks + /// on other internal or external systems. + /// + /// @param _amount the amount of collateral to withdraw. + function withdraw(uint256 _amount) + external + nonReentrant + noContractAllowed + expectInitialized + returns (uint256, uint256) + { + CDP.Data storage _cdp = _cdps[msg.sender]; + require(block.number > _cdp.lastDeposit, ''); + + _cdp.update(_ctx); + + (uint256 _withdrawnAmount, uint256 _decreasedValue) = _withdrawFundsTo( + msg.sender, + _amount + ); + + _cdp.totalDeposited = _cdp.totalDeposited.sub( + _decreasedValue, + 'Exceeds withdrawable amount' + ); + _cdp.checkHealth(_ctx, 'Action blocked: unhealthy collateralization ratio'); + if (_amount >= flushActivator) { + flushActiveVault(); + } + emit TokensWithdrawn(msg.sender, _amount, _withdrawnAmount, _decreasedValue); + + return (_withdrawnAmount, _decreasedValue); + } + + /// @dev Repays debt with the native and or synthetic token. + /// + /// An approval is required to transfer native tokens to the transmuter. + function repay(uint256 _parentAmount, uint256 _childAmount) + external + nonReentrant + noContractAllowed + onPriceCheck + expectInitialized + { + CDP.Data storage _cdp = _cdps[msg.sender]; + _cdp.update(_ctx); + + if (_parentAmount > 0) { + token.safeTransferFrom(msg.sender, address(this), _parentAmount); + _distributeToTransmuter(_parentAmount); + } + + if (_childAmount > 0) { + xtoken.burnFrom(msg.sender, _childAmount); + //lower debt cause burn + xtoken.lowerHasMinted(_childAmount); + } + + uint256 _totalAmount = _parentAmount.add(_childAmount); + _cdp.totalDebt = _cdp.totalDebt.sub(_totalAmount, ''); + + emit TokensRepaid(msg.sender, _parentAmount, _childAmount); + } + + /// @dev Attempts to liquidate part of a CDP's collateral to pay back its debt. + /// + /// @param _amount the amount of collateral to attempt to liquidate. + function liquidate(uint256 _amount) + external + nonReentrant + noContractAllowed + onPriceCheck + expectInitialized + returns (uint256, uint256) + { + CDP.Data storage _cdp = _cdps[msg.sender]; + _cdp.update(_ctx); + + // don't attempt to liquidate more than is possible + if (_amount > _cdp.totalDebt) { + _amount = _cdp.totalDebt; + } + (uint256 _withdrawnAmount, uint256 _decreasedValue) = _withdrawFundsTo( + address(this), + _amount + ); + //changed to new transmuter compatibillity + _distributeToTransmuter(_withdrawnAmount); + + _cdp.totalDeposited = _cdp.totalDeposited.sub(_decreasedValue, ''); + _cdp.totalDebt = _cdp.totalDebt.sub(_withdrawnAmount, ''); + emit TokensLiquidated(msg.sender, _amount, _withdrawnAmount, _decreasedValue); + + return (_withdrawnAmount, _decreasedValue); + } + + /// @dev Mints synthetic tokens by either claiming credit or increasing the debt. + /// + /// Claiming credit will take priority over increasing the debt. + /// + /// This function reverts if the debt is increased and the CDP health check fails. + /// + /// @param _amount the amount of alchemic tokens to borrow. + function mint(uint256 _amount) + external + nonReentrant + notEmergency + noContractAllowed + onPriceCheck + expectInitialized + { + CDP.Data storage _cdp = _cdps[msg.sender]; + _cdp.update(_ctx); + + uint256 _totalCredit = _cdp.totalCredit; + + if (_totalCredit < _amount) { + uint256 _remainingAmount = _amount - _totalCredit; + + if (borrowFee > 0) { + uint256 _borrowFeeAmount = _remainingAmount.mul(borrowFee).div( + PERCENT_RESOLUTION + ); + _remainingAmount = _remainingAmount.add(_borrowFeeAmount); + xtoken.mint(rewards, _borrowFeeAmount); + } + _cdp.totalDebt = _cdp.totalDebt.add(_remainingAmount); + _cdp.totalCredit = 0; + + _cdp.checkHealth(_ctx, 'Alchemist: Loan-to-value ratio breached'); + } else { + _cdp.totalCredit = _totalCredit.sub(_amount); + } + + xtoken.mint(msg.sender, _amount); + if (_amount >= flushActivator) { + flushActiveVault(); + } + } + + /// @dev Gets the number of vaults in the vault list. + /// + /// @return the vault count. + function vaultCount() external view returns (uint256) { + return _vaults.length(); + } + + /// @dev Get the adapter of a vault. + /// + /// @param _vaultId the identifier of the vault. + /// + /// @return the vault adapter. + function getVaultAdapter(uint256 _vaultId) external view returns (IVaultAdapter) { + AlchemistVault.Data storage _vault = _vaults.get(_vaultId); + return _vault.adapter; + } + + /// @dev Get the total amount of the parent asset that has been deposited into a vault. + /// + /// @param _vaultId the identifier of the vault. + /// + /// @return the total amount of deposited tokens. + function getVaultTotalDeposited(uint256 _vaultId) external view returns (uint256) { + AlchemistVault.Data storage _vault = _vaults.get(_vaultId); + return _vault.totalDeposited; + } + + /// @dev Get the total amount of collateral deposited into a CDP. + /// + /// @param _account the user account of the CDP to query. + /// + /// @return the deposited amount of tokens. + function getCdpTotalDeposited(address _account) external view returns (uint256) { + CDP.Data storage _cdp = _cdps[_account]; + return _cdp.totalDeposited; + } + + /// @dev Get the total amount of alchemic tokens borrowed from a CDP. + /// + /// @param _account the user account of the CDP to query. + /// + /// @return the borrowed amount of tokens. + function getCdpTotalDebt(address _account) external view returns (uint256) { + CDP.Data storage _cdp = _cdps[_account]; + return _cdp.getUpdatedTotalDebt(_ctx); + } + + /// @dev Get the total amount of credit that a CDP has. + /// + /// @param _account the user account of the CDP to query. + /// + /// @return the amount of credit. + function getCdpTotalCredit(address _account) external view returns (uint256) { + CDP.Data storage _cdp = _cdps[_account]; + return _cdp.getUpdatedTotalCredit(_ctx); + } + + /// @dev Gets the last recorded block of when a user made a deposit into their CDP. + /// + /// @param _account the user account of the CDP to query. + /// + /// @return the block number of the last deposit. + function getCdpLastDeposit(address _account) external view returns (uint256) { + CDP.Data storage _cdp = _cdps[_account]; + return _cdp.lastDeposit; + } + + /// @dev sends tokens to the transmuter + /// + /// benefit of great nation of transmuter + function _distributeToTransmuter(uint256 amount) internal { + token.approve(transmuter, amount); + ITransmuter(transmuter).distribute(address(this), amount); + // lower debt cause of 'burn' + xtoken.lowerHasMinted(amount); + } + + /// @dev Checks that parent token is on peg. + /// + /// This is used over a modifier limit of pegged interactions. + modifier onPriceCheck() { + if (pegMinimum > 0) { + require( + ICurveToken(address(token)).get_virtual_price() > pegMinimum, + 'off peg limitation' + ); + } + _; + } + /// @dev Checks that caller is not a eoa. + /// + /// This is used to prevent contracts from interacting. + modifier noContractAllowed() { + require( + !address(msg.sender).isContract() && msg.sender == tx.origin, + 'Sorry we do not accept contract!' + ); + _; + } + /// @dev Checks that the contract is in an initialized state. + /// + /// This is used over a modifier to reduce the size of the contract + modifier expectInitialized() { + require(initialized, 'Alchemist: not initialized.'); + _; + } + + /// @dev Checks that the current message sender or caller is the governance address. + /// + /// + modifier onlyGov() { + require(msg.sender == governance, 'Alchemist: only governance.'); + _; + } + + /// @dev Checks that the emergencyExit is not enabled. + /// + /// + modifier notEmergency() { + require(!emergencyExit, 'emergency pause enabled'); + _; + } + + /// @dev Updates the active vault. + /// + /// This function reverts if the vault adapter is the zero address, if the token that the vault adapter accepts + /// is not the token that this contract defines as the parent asset, or if the contract has not yet been initialized. + /// + /// @param _adapter the adapter for the new active vault. + function _updateActiveVault(IVaultAdapter _adapter) internal { + require( + _adapter != IVaultAdapter(ZERO_ADDRESS), + 'Alchemist: active vault address cannot be 0x0.' + ); + require(_adapter.token() == token, 'Alchemist: token mismatch.'); + + _vaults.push(AlchemistVault.Data({adapter: _adapter, totalDeposited: 0})); + + emit ActiveVaultUpdated(_adapter); + } + + /// @dev Recalls an amount of funds from a vault to this contract. + /// + /// @param _vaultId the identifier of the recall funds from. + /// @param _amount the amount of funds to recall from the vault. + /// + /// @return the amount of funds that were recalled from the vault to this contract and the decreased vault value. + function _recallFunds(uint256 _vaultId, uint256 _amount) + internal + returns (uint256, uint256) + { + require( + emergencyExit || msg.sender == governance || _vaultId != _vaults.lastIndex(), + 'Alchemist: not an emergency, not governance, and user does not have permission to recall funds from active vault' + ); + + AlchemistVault.Data storage _vault = _vaults.get(_vaultId); + (uint256 _withdrawnAmount, uint256 _decreasedValue) = _vault.withdraw( + address(this), + _amount + ); + + emit FundsRecalled(_vaultId, _withdrawnAmount, _decreasedValue); + + return (_withdrawnAmount, _decreasedValue); + } + + /// @dev Attempts to withdraw funds from the active vault to the recipient. + /// + /// Funds will be first withdrawn from this contracts balance and then from the active vault. This function + /// is different from `recallFunds` in that it reduces the total amount of deposited tokens by the decreased + /// value of the vault. + /// + /// @param _recipient the account to withdraw the funds to. + /// @param _amount the amount of funds to withdraw. + function _withdrawFundsTo(address _recipient, uint256 _amount) + internal + returns (uint256, uint256) + { + // Pull the funds from the buffer. + uint256 _bufferedAmount = Math.min(_amount, token.balanceOf(address(this))); + + if (_recipient != address(this)) { + token.safeTransfer(_recipient, _bufferedAmount); + } + + uint256 _totalWithdrawn = _bufferedAmount; + uint256 _totalDecreasedValue = _bufferedAmount; + + uint256 _remainingAmount = _amount.sub(_bufferedAmount); + + // Pull the remaining funds from the active vault. + if (_remainingAmount > 0) { + (uint256 _withdrawAmount, uint256 _decreasedValue) = _vaults.last().withdraw( + _recipient, + _remainingAmount + ); + + _totalWithdrawn = _totalWithdrawn.add(_withdrawAmount); + _totalDecreasedValue = _totalDecreasedValue.add(_decreasedValue); + } + + totalDeposited = totalDeposited.sub(_totalDecreasedValue); + + return (_totalWithdrawn, _totalDecreasedValue); + } +} diff --git a/contracts/v3/alchemix/Transmuter.sol b/contracts/v3/alchemix/Transmuter.sol new file mode 100644 index 00000000..99d50cc0 --- /dev/null +++ b/contracts/v3/alchemix/Transmuter.sol @@ -0,0 +1,512 @@ +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; +import './interfaces/IERC20Burnable.sol'; + +import 'hardhat/console.sol'; + +// ___ __ __ _ ___ __ _ +// / _ | / / ____ / / ___ __ _ (_) __ __ / _ \ ____ ___ ___ ___ ___ / /_ ___ (_) +// / __ | / / / __/ / _ \/ -_) / ' \ / / \ \ / / ___/ / __// -_) (_- uint256) public depositedAlTokens; + mapping(address => uint256) public tokensInBucket; + mapping(address => uint256) public realisedTokens; + mapping(address => uint256) public lastDividendPoints; + + mapping(address => bool) public userIsKnown; + mapping(uint256 => address) public userList; + uint256 public nextUser; + + uint256 public totalSupplyAltokens; + uint256 public buffer; + uint256 public lastDepositBlock; + + uint256 public totalDividendPoints; + uint256 public unclaimedDividends; + + /// @dev alchemist addresses whitelisted + mapping(address => bool) public whiteList; + + /// @dev The address of the account which currently has administrative capabilities over this contract. + address public governance; + + /// @dev The address of the pending governance. + address public pendingGovernance; + + event GovernanceUpdated(address governance); + + event PendingGovernanceUpdated(address pendingGovernance); + + event TransmuterPeriodUpdated(uint256 newTransmutationPeriod); + + constructor( + address _AlToken, + address _Token, + address _governance + ) public { + require(_governance != ZERO_ADDRESS, 'Transmuter: 0 gov'); + governance = _governance; + AlToken = _AlToken; + Token = _Token; + transmutationPeriod = 50; + } + + ///@return displays the user's share of the pooled alTokens. + function dividendsOwing(address account) public view returns (uint256) { + uint256 newDividendPoints = totalDividendPoints.sub(lastDividendPoints[account]); + return depositedAlTokens[account].mul(newDividendPoints).div(POINT_MULTIPLIER); + } + + ///@dev modifier to fill the bucket and keep bookkeeping correct incase of increase/decrease in shares + modifier updateAccount(address account) { + uint256 owing = dividendsOwing(account); + if (owing > 0) { + unclaimedDividends = unclaimedDividends.sub(owing); + tokensInBucket[account] = tokensInBucket[account].add(owing); + } + lastDividendPoints[account] = totalDividendPoints; + _; + } + ///@dev modifier add users to userlist. Users are indexed in order to keep track of when a bond has been filled + modifier checkIfNewUser() { + if (!userIsKnown[msg.sender]) { + userList[nextUser] = msg.sender; + userIsKnown[msg.sender] = true; + nextUser++; + } + _; + } + + ///@dev run the phased distribution of the buffered funds + modifier runPhasedDistribution() { + uint256 _lastDepositBlock = lastDepositBlock; + uint256 _currentBlock = block.number; + uint256 _toDistribute = 0; + uint256 _buffer = buffer; + + // check if there is something in bufffer + if (_buffer > 0) { + // NOTE: if last deposit was updated in the same block as the current call + // then the below logic gates will fail + + //calculate diffrence in time + uint256 deltaTime = _currentBlock.sub(_lastDepositBlock); + + // distribute all if bigger than timeframe + if (deltaTime >= transmutationPeriod) { + _toDistribute = _buffer; + } else { + //needs to be bigger than 0 cuzz solidity no decimals + if (_buffer.mul(deltaTime) > transmutationPeriod) { + _toDistribute = _buffer.mul(deltaTime).div(transmutationPeriod); + } + } + + // factually allocate if any needs distribution + if (_toDistribute > 0) { + // remove from buffer + buffer = _buffer.sub(_toDistribute); + + // increase the allocation + increaseAllocations(_toDistribute); + } + } + + // current timeframe is now the last + lastDepositBlock = _currentBlock; + _; + } + + /// @dev A modifier which checks if whitelisted for minting. + modifier onlyWhitelisted() { + require(whiteList[msg.sender], 'Transmuter: !whitelisted'); + _; + } + + /// @dev Checks that the current message sender or caller is the governance address. + /// + /// + modifier onlyGov() { + require(msg.sender == governance, 'Transmuter: !governance'); + _; + } + + ///@dev set the transmutationPeriod variable + /// + /// sets the length (in blocks) of one full distribution phase + function setTransmutationPeriod(uint256 newTransmutationPeriod) public onlyGov { + transmutationPeriod = newTransmutationPeriod; + emit TransmuterPeriodUpdated(transmutationPeriod); + } + + ///@dev claims the base token after it has been transmuted + /// + ///This function reverts if there is no realisedToken balance + function claim() public { + address sender = msg.sender; + require(realisedTokens[sender] > 0); + uint256 value = realisedTokens[sender]; + realisedTokens[sender] = 0; + IERC20Burnable(Token).safeTransfer(sender, value); + } + + ///@dev Withdraws staked alTokens from the transmuter + /// + /// This function reverts if you try to draw more tokens than you deposited + /// + ///@param amount the amount of alTokens to unstake + function unstake(uint256 amount) public runPhasedDistribution updateAccount(msg.sender) { + // by calling this function before transmuting you forfeit your gained allocation + address sender = msg.sender; + require( + depositedAlTokens[sender] >= amount, + 'Transmuter: unstake amount exceeds deposited amount' + ); + depositedAlTokens[sender] = depositedAlTokens[sender].sub(amount); + totalSupplyAltokens = totalSupplyAltokens.sub(amount); + IERC20Burnable(AlToken).safeTransfer(sender, amount); + } + + ///@dev Deposits alTokens into the transmuter + /// + ///@param amount the amount of alTokens to stake + function stake(uint256 amount) + public + runPhasedDistribution + updateAccount(msg.sender) + checkIfNewUser + { + // requires approval of AlToken first + address sender = msg.sender; + //require tokens transferred in; + IERC20Burnable(AlToken).safeTransferFrom(sender, address(this), amount); + totalSupplyAltokens = totalSupplyAltokens.add(amount); + depositedAlTokens[sender] = depositedAlTokens[sender].add(amount); + } + + /// @dev Converts the staked alTokens to the base tokens in amount of the sum of pendingdivs and tokensInBucket + /// + /// once the alToken has been converted, it is burned, and the base token becomes realisedTokens which can be recieved using claim() + /// + /// reverts if there are no pendingdivs or tokensInBucket + function transmute() public runPhasedDistribution updateAccount(msg.sender) { + address sender = msg.sender; + uint256 pendingz = tokensInBucket[sender]; + uint256 diff; + + require(pendingz > 0, 'need to have pending in bucket'); + + tokensInBucket[sender] = 0; + + // check bucket overflow + if (pendingz > depositedAlTokens[sender]) { + diff = pendingz.sub(depositedAlTokens[sender]); + + // remove overflow + pendingz = depositedAlTokens[sender]; + } + + // decrease altokens + depositedAlTokens[sender] = depositedAlTokens[sender].sub(pendingz); + + // BURN ALTOKENS + IERC20Burnable(AlToken).burn(pendingz); + + // adjust total + totalSupplyAltokens = totalSupplyAltokens.sub(pendingz); + + // reallocate overflow + increaseAllocations(diff); + + // add payout + realisedTokens[sender] = realisedTokens[sender].add(pendingz); + } + + /// @dev Executes transmute() on another account that has had more base tokens allocated to it than alTokens staked. + /// + /// The caller of this function will have the surlus base tokens credited to their tokensInBucket balance, rewarding them for performing this action + /// + /// This function reverts if the address to transmute is not over-filled. + /// + /// @param toTransmute address of the account you will force transmute. + function forceTransmute(address toTransmute) + public + runPhasedDistribution + updateAccount(msg.sender) + updateAccount(toTransmute) + { + //load into memory + address sender = msg.sender; + uint256 pendingz = tokensInBucket[toTransmute]; + // check restrictions + require(pendingz > depositedAlTokens[toTransmute], 'Transmuter: !overflow'); + + // empty bucket + tokensInBucket[toTransmute] = 0; + + // calculaate diffrence + uint256 diff = pendingz.sub(depositedAlTokens[toTransmute]); + + // remove overflow + pendingz = depositedAlTokens[toTransmute]; + + // decrease altokens + depositedAlTokens[toTransmute] = 0; + + // BURN ALTOKENS + IERC20Burnable(AlToken).burn(pendingz); + + // adjust total + totalSupplyAltokens = totalSupplyAltokens.sub(pendingz); + + // reallocate overflow + tokensInBucket[sender] = tokensInBucket[sender].add(diff); + + // add payout + realisedTokens[toTransmute] = realisedTokens[toTransmute].add(pendingz); + + // force payout of realised tokens of the toTransmute address + if (realisedTokens[toTransmute] > 0) { + uint256 value = realisedTokens[toTransmute]; + realisedTokens[toTransmute] = 0; + IERC20Burnable(Token).safeTransfer(toTransmute, value); + } + } + + /// @dev Transmutes and unstakes all alTokens + /// + /// This function combines the transmute and unstake functions for ease of use + function exit() external { + transmute(); + uint256 toWithdraw = depositedAlTokens[msg.sender]; + unstake(toWithdraw); + } + + /// @dev Transmutes and claims all converted base tokens. + /// + /// This function combines the transmute and claim functions while leaving your remaining alTokens staked. + function transmuteAndClaim() external { + transmute(); + claim(); + } + + /// @dev Transmutes, claims base tokens, and withdraws alTokens. + /// + /// This function helps users to exit the transmuter contract completely after converting their alTokens to the base pair. + function transmuteClaimAndWithdraw() external { + transmute(); + claim(); + uint256 toWithdraw = depositedAlTokens[msg.sender]; + unstake(toWithdraw); + } + + /// @dev Distributes the base token proportionally to all alToken stakers. + /// + /// This function is meant to be called by the Alchemist contract for when it is sending yield to the transmuter. + /// Anyone can call this and add funds, idk why they would do that though... + /// + /// @param origin the account that is sending the tokens to be distributed. + /// @param amount the amount of base tokens to be distributed to the transmuter. + function distribute(address origin, uint256 amount) + public + onlyWhitelisted + runPhasedDistribution + { + IERC20Burnable(Token).safeTransferFrom(origin, address(this), amount); + buffer = buffer.add(amount); + } + + /// @dev Allocates the incoming yield proportionally to all alToken stakers. + /// + /// @param amount the amount of base tokens to be distributed in the transmuter. + function increaseAllocations(uint256 amount) internal { + if (totalSupplyAltokens > 0 && amount > 0) { + totalDividendPoints = totalDividendPoints.add( + amount.mul(POINT_MULTIPLIER).div(totalSupplyAltokens) + ); + unclaimedDividends = unclaimedDividends.add(amount); + } else { + buffer = buffer.add(amount); + } + } + + /// @dev Gets the status of a user's staking position. + /// + /// The total amount allocated to a user is the sum of pendingdivs and inbucket. + /// + /// @param user the address of the user you wish to query. + /// + /// returns user status + + function userInfo(address user) + public + view + returns ( + uint256 depositedAl, + uint256 pendingdivs, + uint256 inbucket, + uint256 realised + ) + { + uint256 _depositedAl = depositedAlTokens[user]; + uint256 _toDistribute = buffer.mul(block.number.sub(lastDepositBlock)).div( + transmutationPeriod + ); + if (block.number.sub(lastDepositBlock) > transmutationPeriod) { + _toDistribute = buffer; + } + uint256 _pendingdivs = _toDistribute.mul(depositedAlTokens[user]).div( + totalSupplyAltokens + ); + uint256 _inbucket = tokensInBucket[user].add(dividendsOwing(user)); + uint256 _realised = realisedTokens[user]; + return (_depositedAl, _pendingdivs, _inbucket, _realised); + } + + /// @dev Gets the status of multiple users in one call + /// + /// This function is used to query the contract to check for + /// accounts that have overfilled positions in order to check + /// who can be force transmuted. + /// + /// @param from the first index of the userList + /// @param to the last index of the userList + /// + /// returns the userList with their staking status in paginated form. + function getMultipleUserInfo(uint256 from, uint256 to) + public + view + returns (address[] memory theUserList, uint256[] memory theUserData) + { + uint256 i = from; + uint256 delta = to - from; + address[] memory _theUserList = new address[](delta); //user + uint256[] memory _theUserData = new uint256[](delta * 2); //deposited-bucket + uint256 y = 0; + uint256 _toDistribute = buffer.mul(block.number.sub(lastDepositBlock)).div( + transmutationPeriod + ); + if (block.number.sub(lastDepositBlock) > transmutationPeriod) { + _toDistribute = buffer; + } + for (uint256 x = 0; x < delta; x += 1) { + _theUserList[x] = userList[i]; + _theUserData[y] = depositedAlTokens[userList[i]]; + _theUserData[y + 1] = dividendsOwing(userList[i]) + .add(tokensInBucket[userList[i]]) + .add( + _toDistribute.mul(depositedAlTokens[userList[i]]).div(totalSupplyAltokens) + ); + y += 2; + i += 1; + } + return (_theUserList, _theUserData); + } + + /// @dev Gets info on the buffer + /// + /// This function is used to query the contract to get the + /// latest state of the buffer + /// + /// @return _toDistribute the amount ready to be distributed + /// @return _deltaBlocks the amount of time since the last phased distribution + /// @return _buffer the amount in the buffer + function bufferInfo() + public + view + returns ( + uint256 _toDistribute, + uint256 _deltaBlocks, + uint256 _buffer + ) + { + _deltaBlocks = block.number.sub(lastDepositBlock); + _buffer = buffer; + _toDistribute = _buffer.mul(_deltaBlocks).div(transmutationPeriod); + } + + /// @dev Sets the pending governance. + /// + /// This function reverts if the new pending governance is the zero address or the caller is not the current + /// governance. This is to prevent the contract governance being set to the zero address which would deadlock + /// privileged contract functionality. + /// + /// @param _pendingGovernance the new pending governance. + function setPendingGovernance(address _pendingGovernance) external onlyGov { + require(_pendingGovernance != ZERO_ADDRESS, 'Transmuter: 0 gov'); + + pendingGovernance = _pendingGovernance; + + emit PendingGovernanceUpdated(_pendingGovernance); + } + + /// @dev Accepts the role as governance. + /// + /// This function reverts if the caller is not the new pending governance. + function acceptGovernance() external { + require(msg.sender == pendingGovernance, '!pendingGovernance'); + address _pendingGovernance = pendingGovernance; + governance = _pendingGovernance; + + emit GovernanceUpdated(_pendingGovernance); + } + + /// This function reverts if the caller is not governance + /// + /// @param _toWhitelist the account to mint tokens to. + /// @param _state the whitelist state. + + function setWhitelist(address _toWhitelist, bool _state) external onlyGov { + whiteList[_toWhitelist] = _state; + } +} diff --git a/contracts/v3/alchemix/adapters/YaxisVaultAdapter.sol b/contracts/v3/alchemix/adapters/YaxisVaultAdapter.sol new file mode 100644 index 00000000..33425a7c --- /dev/null +++ b/contracts/v3/alchemix/adapters/YaxisVaultAdapter.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import 'hardhat/console.sol'; + +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol'; + +import {IDetailedERC20} from '../interfaces/IDetailedERC20.sol'; +import {IVaultAdapter} from '../interfaces/IVaultAdapter.sol'; +import {IVault} from '../../interfaces/IVault.sol'; + +/// @title YaxisVaultAdapter +/// +/// @dev A vault adapter implementation which wraps a yAxis vault. +contract YaxisVaultAdapter is IVaultAdapter { + using SafeERC20 for IDetailedERC20; + using SafeMath for uint256; + + /// @dev The vault that the adapter is wrapping. + IVault public immutable vault; + + /// @dev The address which has admin control over this contract. + address public immutable admin; + + /// @dev The token that the vault accepts + IDetailedERC20 public immutable override token; + + constructor(IVault _vault, address _admin) public { + require(_admin != address(0), 'YaxisVaultAdapter: admin address cannot be 0x0.'); + + vault = _vault; + admin = _admin; + IDetailedERC20 _token = IDetailedERC20(_vault.getToken()); + token = _token; + _token.safeApprove(address(_vault), uint256(-1)); + } + + /// @dev Gets the total value of the assets that the adapter holds in the vault. + /// + /// @return the total assets. + function totalValue() external view override returns (uint256) { + return _sharesToTokens(IDetailedERC20(vault.getLPToken()).balanceOf(address(this))); + } + + /// @dev Deposits tokens into the vault. + /// + /// @param _amount the amount of tokens to deposit into the vault. + function deposit(uint256 _amount) external override { + vault.deposit(_amount); + } + + /// @dev Withdraws tokens from the vault to the recipient. + /// + /// This function reverts if the caller is not the admin. + /// + /// @param _recipient the account to withdraw the tokes to. + /// @param _amount the amount of tokens to withdraw. + function withdraw(address _recipient, uint256 _amount) external override { + require(admin == msg.sender, 'YaxisVaultAdapter: only admin'); + + IDetailedERC20 _token = token; + uint256 beforeBalance = _token.balanceOf(address(this)); + + vault.withdraw(_tokensToShares(_amount)); + + _token.safeTransfer(_recipient, _token.balanceOf(address(this)) - beforeBalance); + } + + /// @dev Computes the number of tokens an amount of shares is worth. + /// + /// @param _sharesAmount the amount of shares. + /// + /// @return the number of tokens the shares are worth. + + function _sharesToTokens(uint256 _sharesAmount) internal view returns (uint256) { + return _sharesAmount.mul(vault.getPricePerFullShare()).div(1e18); + } + + /// @dev Computes the number of shares an amount of tokens is worth. + /// + /// @param _tokensAmount the amount of shares. + /// + /// @return the number of shares the tokens are worth. + function _tokensToShares(uint256 _tokensAmount) internal view returns (uint256) { + return _tokensAmount.mul(1e18).div(vault.getPricePerFullShare()); + } +} diff --git a/contracts/v3/alchemix/adapters/YearnVaultAdapter.sol b/contracts/v3/alchemix/adapters/YearnVaultAdapter.sol new file mode 100644 index 00000000..01e2165e --- /dev/null +++ b/contracts/v3/alchemix/adapters/YearnVaultAdapter.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import 'hardhat/console.sol'; + +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol'; + +import {IDetailedERC20} from '../interfaces/IDetailedERC20.sol'; +import {IVaultAdapter} from '../interfaces/IVaultAdapter.sol'; +import {IyVaultV2} from '../interfaces/IyVaultV2.sol'; + +/// @title YearnVaultAdapter +/// +/// @dev A vault adapter implementation which wraps a yEarn vault. +contract YearnVaultAdapter is IVaultAdapter { + using SafeERC20 for IDetailedERC20; + using SafeMath for uint256; + + /// @dev The vault that the adapter is wrapping. + IyVaultV2 public vault; + + /// @dev The address which has admin control over this contract. + address public admin; + + /// @dev The decimals of the token. + uint256 public decimals; + + constructor(IyVaultV2 _vault, address _admin) public { + vault = _vault; + admin = _admin; + updateApproval(); + decimals = _vault.decimals(); + } + + /// @dev A modifier which reverts if the caller is not the admin. + modifier onlyAdmin() { + require(admin == msg.sender, 'YearnVaultAdapter: only admin'); + _; + } + + /// @dev Gets the token that the vault accepts. + /// + /// @return the accepted token. + function token() external view override returns (IDetailedERC20) { + return IDetailedERC20(vault.token()); + } + + /// @dev Gets the total value of the assets that the adapter holds in the vault. + /// + /// @return the total assets. + function totalValue() external view override returns (uint256) { + return _sharesToTokens(vault.balanceOf(address(this))); + } + + /// @dev Deposits tokens into the vault. + /// + /// @param _amount the amount of tokens to deposit into the vault. + function deposit(uint256 _amount) external override { + vault.deposit(_amount); + } + + /// @dev Withdraws tokens from the vault to the recipient. + /// + /// This function reverts if the caller is not the admin. + /// + /// @param _recipient the account to withdraw the tokes to. + /// @param _amount the amount of tokens to withdraw. + function withdraw(address _recipient, uint256 _amount) external override onlyAdmin { + vault.withdraw(_tokensToShares(_amount), _recipient); + } + + /// @dev Updates the vaults approval of the token to be the maximum value. + function updateApproval() public { + address _token = vault.token(); + IDetailedERC20(_token).safeApprove(address(vault), uint256(-1)); + } + + /// @dev Computes the number of tokens an amount of shares is worth. + /// + /// @param _sharesAmount the amount of shares. + /// + /// @return the number of tokens the shares are worth. + + function _sharesToTokens(uint256 _sharesAmount) internal view returns (uint256) { + return _sharesAmount.mul(vault.pricePerShare()).div(10**decimals); + } + + /// @dev Computes the number of shares an amount of tokens is worth. + /// + /// @param _tokensAmount the amount of shares. + /// + /// @return the number of shares the tokens are worth. + function _tokensToShares(uint256 _tokensAmount) internal view returns (uint256) { + return _tokensAmount.mul(10**decimals).div(vault.pricePerShare()); + } +} diff --git a/contracts/v3/alchemix/interfaces/IChainlink.sol b/contracts/v3/alchemix/interfaces/IChainlink.sol new file mode 100644 index 00000000..bad41d5d --- /dev/null +++ b/contracts/v3/alchemix/interfaces/IChainlink.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + +interface IChainlink { + function latestAnswer() external view returns (int256); +} \ No newline at end of file diff --git a/contracts/v3/alchemix/interfaces/ICurveToken.sol b/contracts/v3/alchemix/interfaces/ICurveToken.sol new file mode 100644 index 00000000..f0ac0a53 --- /dev/null +++ b/contracts/v3/alchemix/interfaces/ICurveToken.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + +interface ICurveToken { + function get_virtual_price() external view returns (uint256); +} diff --git a/contracts/v3/alchemix/interfaces/IDetailedERC20.sol b/contracts/v3/alchemix/interfaces/IDetailedERC20.sol new file mode 100644 index 00000000..115c4f8d --- /dev/null +++ b/contracts/v3/alchemix/interfaces/IDetailedERC20.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IDetailedERC20 is IERC20 { + function name() external returns (string memory); + function symbol() external returns (string memory); + function decimals() external returns (uint8); +} \ No newline at end of file diff --git a/contracts/v3/alchemix/interfaces/IERC20Burnable.sol b/contracts/v3/alchemix/interfaces/IERC20Burnable.sol new file mode 100644 index 00000000..0164c31a --- /dev/null +++ b/contracts/v3/alchemix/interfaces/IERC20Burnable.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.8; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IERC20Burnable is IERC20 { + function burn(uint256 amount) external; + function burnFrom(address account, uint256 amount) external; +} \ No newline at end of file diff --git a/contracts/v3/alchemix/interfaces/IMintableERC20.sol b/contracts/v3/alchemix/interfaces/IMintableERC20.sol new file mode 100644 index 00000000..871bace0 --- /dev/null +++ b/contracts/v3/alchemix/interfaces/IMintableERC20.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + + +import {IDetailedERC20} from "./IDetailedERC20.sol"; + +interface IMintableERC20 is IDetailedERC20{ + function mint(address _recipient, uint256 _amount) external; + function burnFrom(address account, uint256 amount) external; + function lowerHasMinted(uint256 amount)external; +} diff --git a/contracts/v3/alchemix/interfaces/ITransmuter.sol b/contracts/v3/alchemix/interfaces/ITransmuter.sol new file mode 100644 index 00000000..48f2eee3 --- /dev/null +++ b/contracts/v3/alchemix/interfaces/ITransmuter.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + +interface ITransmuter { + function distribute (address origin, uint256 amount) external; +} \ No newline at end of file diff --git a/contracts/v3/alchemix/interfaces/IVaultAdapter.sol b/contracts/v3/alchemix/interfaces/IVaultAdapter.sol new file mode 100644 index 00000000..8d56b7e9 --- /dev/null +++ b/contracts/v3/alchemix/interfaces/IVaultAdapter.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./IDetailedERC20.sol"; + +/// Interface for all Vault Adapter implementations. +interface IVaultAdapter { + + /// @dev Gets the token that the adapter accepts. + function token() external view returns (IDetailedERC20); + + /// @dev The total value of the assets deposited into the vault. + function totalValue() external view returns (uint256); + + /// @dev Deposits funds into the vault. + /// + /// @param _amount the amount of funds to deposit. + function deposit(uint256 _amount) external; + + /// @dev Attempts to withdraw funds from the wrapped vault. + /// + /// The amount withdrawn to the recipient may be less than the amount requested. + /// + /// @param _recipient the recipient of the funds. + /// @param _amount the amount of funds to withdraw. + function withdraw(address _recipient, uint256 _amount) external; +} \ No newline at end of file diff --git a/contracts/v3/alchemix/interfaces/IYearnController.sol b/contracts/v3/alchemix/interfaces/IYearnController.sol new file mode 100644 index 00000000..6c67a6d5 --- /dev/null +++ b/contracts/v3/alchemix/interfaces/IYearnController.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + +interface IYearnController { + function balanceOf(address _token) external view returns (uint256); + function earn(address _token, uint256 _amount) external; + function withdraw(address _token, uint256 _withdrawAmount) external; +} \ No newline at end of file diff --git a/contracts/v3/alchemix/interfaces/IYearnVault.sol b/contracts/v3/alchemix/interfaces/IYearnVault.sol new file mode 100644 index 00000000..42f93f6f --- /dev/null +++ b/contracts/v3/alchemix/interfaces/IYearnVault.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IDetailedERC20} from "./IDetailedERC20.sol"; + +interface IYearnVault { + function balanceOf(address user) external view returns (uint); + function pricePerShare() external view returns (uint); + function deposit(uint amount) external returns (uint); + function withdraw(uint shares, address recipient) external returns (uint); + function token() external view returns (IDetailedERC20); + function totalAssets() external view returns (uint); + function decimals() external view returns (uint8); +} \ No newline at end of file diff --git a/contracts/v3/alchemix/interfaces/IyVaultV2.sol b/contracts/v3/alchemix/interfaces/IyVaultV2.sol new file mode 100644 index 00000000..a097291e --- /dev/null +++ b/contracts/v3/alchemix/interfaces/IyVaultV2.sol @@ -0,0 +1,47 @@ +pragma solidity ^0.6.12; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IyVaultV2 is IERC20 { + function token() external view returns (address); + function deposit() external returns (uint); + function deposit(uint) external returns (uint); + function deposit(uint, address) external returns (uint); + function withdraw() external returns (uint); + function withdraw(uint) external returns (uint); + function withdraw(uint, address) external returns (uint); + function withdraw(uint, address, uint) external returns (uint); + function permit(address, address, uint, uint, bytes32) external view returns (bool); + function pricePerShare() external view returns (uint); + + function apiVersion() external view returns (string memory); + function totalAssets() external view returns (uint); + function maxAvailableShares() external view returns (uint); + function debtOutstanding() external view returns (uint); + function debtOutstanding(address strategy) external view returns (uint); + function creditAvailable() external view returns (uint); + function creditAvailable(address strategy) external view returns (uint); + function availableDepositLimit() external view returns (uint); + function expectedReturn() external view returns (uint); + function expectedReturn(address strategy) external view returns (uint); + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint); + function balanceOf(address owner) external view override returns (uint); + function totalSupply() external view override returns (uint); + function governance() external view returns (address); + function management() external view returns (address); + function guardian() external view returns (address); + function guestList() external view returns (address); + function strategies(address) external view returns (uint, uint, uint, uint, uint, uint, uint, uint); + function withdrawalQueue(uint) external view returns (address); + function emergencyShutdown() external view returns (bool); + function depositLimit() external view returns (uint); + function debtRatio() external view returns (uint); + function totalDebt() external view returns (uint); + function lastReport() external view returns (uint); + function activation() external view returns (uint); + function rewards() external view returns (address); + function managementFee() external view returns (uint); + function performanceFee() external view returns (uint); +} \ No newline at end of file diff --git a/contracts/v3/alchemix/libraries/FixedPointMath.sol b/contracts/v3/alchemix/libraries/FixedPointMath.sol new file mode 100644 index 00000000..b55a211a --- /dev/null +++ b/contracts/v3/alchemix/libraries/FixedPointMath.sol @@ -0,0 +1,68 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.6.12; + +library FixedPointMath { + uint256 public constant DECIMALS = 18; + uint256 public constant SCALAR = 10**DECIMALS; + + struct FixedDecimal { + uint256 x; + } + + function fromU256(uint256 value) internal pure returns (FixedDecimal memory) { + uint256 x; + require(value == 0 || (x = value * SCALAR) / SCALAR == value); + return FixedDecimal(x); + } + + function maximumValue() internal pure returns (FixedDecimal memory) { + return FixedDecimal(uint256(-1)); + } + + function add(FixedDecimal memory self, FixedDecimal memory value) internal pure returns (FixedDecimal memory) { + uint256 x; + require((x = self.x + value.x) >= self.x); + return FixedDecimal(x); + } + + function add(FixedDecimal memory self, uint256 value) internal pure returns (FixedDecimal memory) { + return add(self, fromU256(value)); + } + + function sub(FixedDecimal memory self, FixedDecimal memory value) internal pure returns (FixedDecimal memory) { + uint256 x; + require((x = self.x - value.x) <= self.x); + return FixedDecimal(x); + } + + function sub(FixedDecimal memory self, uint256 value) internal pure returns (FixedDecimal memory) { + return sub(self, fromU256(value)); + } + + function mul(FixedDecimal memory self, uint256 value) internal pure returns (FixedDecimal memory) { + uint256 x; + require(value == 0 || (x = self.x * value) / value == self.x); + return FixedDecimal(x); + } + + function div(FixedDecimal memory self, uint256 value) internal pure returns (FixedDecimal memory) { + require(value != 0); + return FixedDecimal(self.x / value); + } + + function cmp(FixedDecimal memory self, FixedDecimal memory value) internal pure returns (int256) { + if (self.x < value.x) { + return -1; + } + + if (self.x > value.x) { + return 1; + } + + return 0; + } + + function decode(FixedDecimal memory self) internal pure returns (uint256) { + return self.x / SCALAR; + } +} \ No newline at end of file diff --git a/contracts/v3/alchemix/libraries/alchemist/AlchemistVault.sol b/contracts/v3/alchemix/libraries/alchemist/AlchemistVault.sol new file mode 100644 index 00000000..977c4a06 --- /dev/null +++ b/contracts/v3/alchemix/libraries/alchemist/AlchemistVault.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + +//import "hardhat/console.sol"; + +import {Math} from '@openzeppelin/contracts/math/Math.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol'; +import {IDetailedERC20} from '../../interfaces/IDetailedERC20.sol'; +import {IVaultAdapter} from '../../interfaces/IVaultAdapter.sol'; +import 'hardhat/console.sol'; + +/// @title Pool +/// +/// @dev A library which provides the AlchemistVault data struct and associated functions. +library AlchemistVault { + using AlchemistVault for Data; + using AlchemistVault for List; + using SafeERC20 for IDetailedERC20; + using SafeMath for uint256; + + struct Data { + IVaultAdapter adapter; + uint256 totalDeposited; + } + + struct List { + Data[] elements; + } + + /// @dev Gets the total amount of assets deposited in the vault. + /// + /// @return the total assets. + function totalValue(Data storage _self) internal view returns (uint256) { + return _self.adapter.totalValue(); + } + + /// @dev Gets the token that the vault accepts. + /// + /// @return the accepted token. + function token(Data storage _self) internal view returns (IDetailedERC20) { + return IDetailedERC20(_self.adapter.token()); + } + + /// @dev Deposits funds from the caller into the vault. + /// + /// @param _amount the amount of funds to deposit. + function deposit(Data storage _self, uint256 _amount) internal returns (uint256) { + // Push the token that the vault accepts onto the stack to save gas. + IDetailedERC20 _token = _self.token(); + + _token.safeTransfer(address(_self.adapter), _amount); + _self.adapter.deposit(_amount); + _self.totalDeposited = _self.totalDeposited.add(_amount); + + return _amount; + } + + /// @dev Deposits the entire token balance of the caller into the vault. + function depositAll(Data storage _self) internal returns (uint256) { + IDetailedERC20 _token = _self.token(); + return _self.deposit(_token.balanceOf(address(this))); + } + + /// @dev Withdraw deposited funds from the vault. + /// + /// @param _recipient the account to withdraw the tokens to. + /// @param _amount the amount of tokens to withdraw. + function withdraw( + Data storage _self, + address _recipient, + uint256 _amount + ) internal returns (uint256, uint256) { + (uint256 _withdrawnAmount, uint256 _decreasedValue) = _self.directWithdraw( + _recipient, + _amount + ); + _self.totalDeposited = _self.totalDeposited.sub(_decreasedValue); + return (_withdrawnAmount, _decreasedValue); + } + + /// @dev Directly withdraw deposited funds from the vault. + /// + /// @param _recipient the account to withdraw the tokens to. + /// @param _amount the amount of tokens to withdraw. + function directWithdraw( + Data storage _self, + address _recipient, + uint256 _amount + ) internal returns (uint256, uint256) { + IDetailedERC20 _token = _self.token(); + + uint256 _startingBalance = _token.balanceOf(_recipient); + uint256 _startingTotalValue = _self.totalValue(); + + _self.adapter.withdraw(_recipient, _amount); + + uint256 _withdrawnAmount = _token.balanceOf(_recipient).sub(_startingBalance); + uint256 _decreasedValue = _startingTotalValue.sub(_self.totalValue()); + + return (_withdrawnAmount, _decreasedValue); + } + + /// @dev Withdraw all the deposited funds from the vault. + /// + /// @param _recipient the account to withdraw the tokens to. + function withdrawAll(Data storage _self, address _recipient) + internal + returns (uint256, uint256) + { + return _self.withdraw(_recipient, _self.totalDeposited); + } + + /// @dev Harvests yield from the vault. + /// + /// @param _recipient the account to withdraw the harvested yield to. + function harvest(Data storage _self, address _recipient) + internal + returns (uint256, uint256) + { + if (_self.totalValue() <= _self.totalDeposited) { + return (0, 0); + } + uint256 _withdrawAmount = _self.totalValue().sub(_self.totalDeposited); + return _self.directWithdraw(_recipient, _withdrawAmount); + } + + /// @dev Adds a element to the list. + /// + /// @param _element the element to add. + function push(List storage _self, Data memory _element) internal { + _self.elements.push(_element); + } + + /// @dev Gets a element from the list. + /// + /// @param _index the index in the list. + /// + /// @return the element at the specified index. + function get(List storage _self, uint256 _index) internal view returns (Data storage) { + return _self.elements[_index]; + } + + /// @dev Gets the last element in the list. + /// + /// This function will revert if there are no elements in the list. + /// + /// @return the last element in the list. + function last(List storage _self) internal view returns (Data storage) { + return _self.elements[_self.lastIndex()]; + } + + /// @dev Gets the index of the last element in the list. + /// + /// This function will revert if there are no elements in the list. + /// + /// @return the index of the last element. + function lastIndex(List storage _self) internal view returns (uint256) { + uint256 _length = _self.length(); + return _length.sub(1, 'AlchemistVault.List: empty'); + } + + /// @dev Gets the number of elements in the list. + /// + /// @return the number of elements. + function length(List storage _self) internal view returns (uint256) { + return _self.elements.length; + } +} diff --git a/contracts/v3/alchemix/libraries/alchemist/CDP.sol b/contracts/v3/alchemix/libraries/alchemist/CDP.sol new file mode 100644 index 00000000..d305f7da --- /dev/null +++ b/contracts/v3/alchemix/libraries/alchemist/CDP.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + +import {Math} from '@openzeppelin/contracts/math/Math.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol'; + +import {FixedPointMath} from '../FixedPointMath.sol'; +import {IDetailedERC20} from '../../interfaces/IDetailedERC20.sol'; +import 'hardhat/console.sol'; + +/// @title CDP +/// +/// @dev A library which provides the CDP data struct and associated functions. +library CDP { + using CDP for Data; + using FixedPointMath for FixedPointMath.FixedDecimal; + using SafeERC20 for IDetailedERC20; + using SafeMath for uint256; + + struct Context { + FixedPointMath.FixedDecimal collateralizationLimit; + FixedPointMath.FixedDecimal accumulatedYieldWeight; + } + + struct Data { + uint256 totalDeposited; + uint256 totalDebt; + uint256 totalCredit; + uint256 lastDeposit; + FixedPointMath.FixedDecimal lastAccumulatedYieldWeight; + } + + function update(Data storage _self, Context storage _ctx) internal { + uint256 _earnedYield = _self.getEarnedYield(_ctx); + if (_earnedYield > _self.totalDebt) { + _self.totalCredit = _earnedYield.sub(_self.totalDebt); + _self.totalDebt = 0; + } else { + _self.totalDebt = _self.totalDebt.sub(_earnedYield); + } + _self.lastAccumulatedYieldWeight = _ctx.accumulatedYieldWeight; + } + + /// @dev Assures that the CDP is healthy. + /// + /// This function will revert if the CDP is unhealthy. + function checkHealth( + Data storage _self, + Context storage _ctx, + string memory _msg + ) internal view { + require(_self.isHealthy(_ctx), _msg); + } + + /// @dev Gets if the CDP is considered healthy. + /// + /// A CDP is healthy if its collateralization ratio is greater than the global collateralization limit. + /// + /// @return if the CDP is healthy. + function isHealthy(Data storage _self, Context storage _ctx) internal view returns (bool) { + return _ctx.collateralizationLimit.cmp(_self.getCollateralizationRatio(_ctx)) <= 0; + } + + function getUpdatedTotalDebt(Data storage _self, Context storage _ctx) + internal + view + returns (uint256) + { + uint256 _unclaimedYield = _self.getEarnedYield(_ctx); + uint256 _currentTotalDebt = _self.totalDebt; + + if (_unclaimedYield < _currentTotalDebt) { + return _currentTotalDebt - _unclaimedYield; + } + + return 0; + } + + function getUpdatedTotalCredit(Data storage _self, Context storage _ctx) + internal + view + returns (uint256) + { + uint256 _unclaimedYield = _self.getEarnedYield(_ctx); + if (_unclaimedYield == 0) { + return _self.totalCredit; + } + + uint256 _currentTotalDebt = _self.totalDebt; + if (_unclaimedYield <= _currentTotalDebt) { + return 0; + } + + return _self.totalCredit + (_unclaimedYield - _currentTotalDebt); + } + + /// @dev Gets the amount of yield that a CDP has earned since the last time it was updated. + /// + /// @param _self the CDP to query. + /// @param _ctx the CDP context. + /// + /// @return the amount of earned yield. + function getEarnedYield(Data storage _self, Context storage _ctx) + internal + view + returns (uint256) + { + FixedPointMath.FixedDecimal memory _currentAccumulatedYieldWeight = _ctx + .accumulatedYieldWeight; + FixedPointMath.FixedDecimal memory _lastAccumulatedYieldWeight = _self + .lastAccumulatedYieldWeight; + + if (_currentAccumulatedYieldWeight.cmp(_lastAccumulatedYieldWeight) == 0) { + return 0; + } + + return + _currentAccumulatedYieldWeight + .sub(_lastAccumulatedYieldWeight) + .mul(_self.totalDeposited) + .decode(); + } + + /// @dev Gets a CDPs collateralization ratio. + /// + /// The collateralization ratio is defined as the ratio of collateral to debt. If the CDP has zero debt then this + /// will return the maximum value of a fixed point integer. + /// + /// This function will use the updated total debt so an update before calling this function is not required. + /// + /// @param _self the CDP to query. + /// + /// @return a fixed point integer representing the collateralization ratio. + function getCollateralizationRatio(Data storage _self, Context storage _ctx) + internal + view + returns (FixedPointMath.FixedDecimal memory) + { + uint256 _totalDebt = _self.getUpdatedTotalDebt(_ctx); + if (_totalDebt == 0) { + return FixedPointMath.maximumValue(); + } + return FixedPointMath.fromU256(_self.totalDeposited).div(_totalDebt); + } +} diff --git a/contracts/v3/alchemix/mocks/ERC20Mock.sol b/contracts/v3/alchemix/mocks/ERC20Mock.sol new file mode 100644 index 00000000..7d7eec76 --- /dev/null +++ b/contracts/v3/alchemix/mocks/ERC20Mock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/// @title ERC20Mock +/// +/// @dev A mock of an ERC20 token which lets anyone burn and mint tokens. +contract ERC20Mock is ERC20 { + + constructor(string memory _name, string memory _symbol, uint8 _decimals) public ERC20(_name, _symbol) { + _setupDecimals(_decimals); + } + + function mint(address _recipient, uint256 _amount) external { + _mint(_recipient, _amount); + } + + function burn(address _account, uint256 _amount) external { + _burn(_account, _amount); + } +} diff --git a/contracts/v3/alchemix/mocks/VaultAdapterMock.sol b/contracts/v3/alchemix/mocks/VaultAdapterMock.sol new file mode 100644 index 00000000..b3cc6b91 --- /dev/null +++ b/contracts/v3/alchemix/mocks/VaultAdapterMock.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +import "../interfaces/IVaultAdapter.sol"; + +contract VaultAdapterMock is IVaultAdapter { + using SafeERC20 for IDetailedERC20; + + IDetailedERC20 private _token; + + constructor(IDetailedERC20 token_) public { + _token = token_; + } + + function token() external view override returns (IDetailedERC20) { + return _token; + } + + function totalValue() external view override returns (uint256) { + return _token.balanceOf(address(this)); + } + + function deposit(uint256 _amount) external override { } + + function withdraw(address _recipient, uint256 _amount) external override { + _token.safeTransfer(_recipient, _amount); + } +} diff --git a/contracts/v3/alchemix/mocks/YearnControllerMock.sol b/contracts/v3/alchemix/mocks/YearnControllerMock.sol new file mode 100644 index 00000000..c1862e55 --- /dev/null +++ b/contracts/v3/alchemix/mocks/YearnControllerMock.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import 'hardhat/console.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol'; + +import '../interfaces/IYearnController.sol'; + +contract YearnControllerMock is IYearnController { + using SafeERC20 for IERC20; + using SafeMath for uint256; + + address public constant blackhole = 0x000000000000000000000000000000000000dEaD; + + uint256 public withdrawalFee = 50; + uint256 public constant withdrawalMax = 10000; + + function setWithdrawalFee(uint256 _withdrawalFee) external { + withdrawalFee = _withdrawalFee; + } + + function balanceOf(address _token) external view override returns (uint256) { + return IERC20(_token).balanceOf(address(this)); + } + + function earn(address _token, uint256 _amount) external override {} + + function withdraw(address _token, uint256 _amount) external override { + uint256 _balance = IERC20(_token).balanceOf(address(this)); + // uint _fee = _amount.mul(withdrawalFee).div(withdrawalMax); + + // IERC20(_token).safeTransfer(blackhole, _fee); + IERC20(_token).safeTransfer(msg.sender, _amount); + } +} diff --git a/contracts/v3/alchemix/mocks/YearnVaultMock.sol b/contracts/v3/alchemix/mocks/YearnVaultMock.sol new file mode 100644 index 00000000..9f38df66 --- /dev/null +++ b/contracts/v3/alchemix/mocks/YearnVaultMock.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import 'hardhat/console.sol'; + +import '@openzeppelin/contracts/math/SafeMath.sol'; +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; + +import '../interfaces/IYearnController.sol'; +import '../interfaces/IYearnVault.sol'; + +contract YearnVaultMock is ERC20 { + using SafeERC20 for IDetailedERC20; + using SafeMath for uint256; + + uint256 public min = 9500; + uint256 public constant max = 10000; + + IYearnController public controller; + IDetailedERC20 public token; + + constructor(IDetailedERC20 _token, IYearnController _controller) + public + ERC20('yEarn Mock', 'yMOCK') + { + token = _token; + controller = _controller; + } + + function vdecimals() external view returns (uint8) { + return decimals(); + } + + function balance() public view returns (uint256) { + return token.balanceOf(address(this)).add(controller.balanceOf(address(token))); + } + + function available() public view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() external { + uint256 _bal = available(); + token.safeTransfer(address(controller), _bal); + controller.earn(address(token), _bal); + } + + function deposit(uint256 _amount) external returns (uint256) { + 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 withdraw(uint256 _shares, address _recipient) external returns (uint256) { + 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); + 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(_recipient, _r); + } + + function pricePerShare() external view returns (uint256) { + return balance().mul(1e18).div(totalSupply()); + } // changed to v2 + + /// @dev This is not part of the vault contract and is meant for quick debugging contracts to have control over + /// completely clearing the vault buffer to test certain behaviors better. + function clear() external { + token.safeTransfer(address(controller), token.balanceOf(address(this))); + controller.earn(address(token), token.balanceOf(address(this))); + } +} diff --git a/deploy/v3/9.Alchemist.js b/deploy/v3/9.Alchemist.js new file mode 100644 index 00000000..9a5e3f86 --- /dev/null +++ b/deploy/v3/9.Alchemist.js @@ -0,0 +1,60 @@ +module.exports = async ({ getNamedAccounts, deployments }) => { + const { deploy, execute } = deployments; + let { deployer, MIMCRV } = await getNamedAccounts(); + const Vault = await deployments.get('Vault3CRV'); // TODO: need to use MIM Vault + + const AlToken = await deploy('AlToken', { + from: deployer, + log: true, + args: [] + }); + + const YaxisVaultAdapter = await deploy('YaxisVaultAdapter', { + from: deployer, + log: true, + args: [Vault.address, deployer] + }); + + const Alchemist = await deploy('Alchemist', { + from: deployer, + log: true, + args: [MIMCRV, AlToken.address, deployer, deployer] + }); + + const Transmuter = await deploy('Transmuter', { + from: deployer, + log: true, + args: [AlToken.address, MIMCRV, deployer] + }); + + await execute( + 'AlToken', + { from: deployer, log: true }, + 'setWhitelist', + Alchemist.address, + true + ); + await execute( + 'Transmuter', + { from: deployer, log: true }, + 'setWhitelist', + Alchemist.address, + true + ); + await execute('Alchemist', { from: deployer, log: true }, 'setRewards', deployer); + await execute( + 'Alchemist', + { from: deployer, log: true }, + 'setTransmuter', + Transmuter.address + ); + + await execute( + 'Alchemist', + { from: deployer, log: true }, + 'initialize', + YaxisVaultAdapter.address + ); +}; + +module.exports.tags = ['Alchemist']; diff --git a/test/helpers/utils.js b/test/helpers/utils.js new file mode 100644 index 00000000..0021ebb6 --- /dev/null +++ b/test/helpers/utils.js @@ -0,0 +1,48 @@ +const { BigNumber } = require('ethers'); + +const ONE = BigNumber.from(1); +exports.MAXIMUM_U32 = ONE.shl(31); +exports.MAXIMUM_U256 = ONE.shl(255); +exports.ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +const mine = async (provider) => { + return provider.send('evm_mine', []); +}; + +exports.snapshot = async (provider) => { + await provider.send('evm_snapshot', []); + return await mine(provider); +}; + +exports.revert = async (provider, snapshotId) => { + return await provider.send('evm_revert', [snapshotId]); +}; + +exports.increaseTime = async (provider, seconds) => { + return provider.send('evm_increaseTime', [seconds]); +}; + +exports.setNextBlockTime = async (provider, time) => { + return provider.send('evm_setNextBlockTimestamp', [time.unix()]); +}; + +exports.mineBlocks = async (provider, numberBlocks) => { + for (let i = 0; i < numberBlocks; i++) { + await provider.send('evm_mine', []); + } + return Promise.resolve(); +}; + +const feeOn = (value, numerator, resolution) => { + return ONE.mul(value).mul(numerator).div(resolution); +}; + +exports.takeFee = (value, numerator, resolution) => { + return ONE.mul(value).sub(feeOn(value, numerator, resolution)); +}; + +exports.delay = (ms) => new Promise((res) => setTimeout(res, ms)); + +exports.ONE = ONE; +exports.mine = mine; +exports.feeOn = feeOn; diff --git a/test/v3/Alchemist.test.js b/test/v3/Alchemist.test.js new file mode 100644 index 00000000..e0e1c21d --- /dev/null +++ b/test/v3/Alchemist.test.js @@ -0,0 +1,937 @@ +const chai = require('chai'); +const { solidity } = require('ethereum-waffle'); +const { ethers } = require('hardhat'); +const { parseEther } = require('ethers/lib/utils'); +const { ZERO_ADDRESS } = require('../helpers/utils'); + +chai.use(solidity); + +const { expect } = chai; + +let AlchemistFactory; +let AlUSDFactory; +let ERC20MockFactory; +let VaultAdapterMockFactory; +let TransmuterFactory; +let YearnVaultAdapterFactory; +let YearnVaultMockFactory; +let YearnControllerMockFactory; + +describe('Alchemist', () => { + let signers; + + before(async () => { + AlchemistFactory = await ethers.getContractFactory('Alchemist'); + TransmuterFactory = await ethers.getContractFactory('Transmuter'); + AlUSDFactory = await ethers.getContractFactory('AlToken'); + ERC20MockFactory = await ethers.getContractFactory('ERC20Mock'); + VaultAdapterMockFactory = await ethers.getContractFactory('VaultAdapterMock'); + YearnVaultAdapterFactory = await ethers.getContractFactory('YearnVaultAdapter'); + YearnVaultMockFactory = await ethers.getContractFactory('YearnVaultMock'); + YearnControllerMockFactory = await ethers.getContractFactory('YearnControllerMock'); + }); + + beforeEach(async () => { + signers = await ethers.getSigners(); + }); + + describe('constructor', async () => { + let deployer; + let sentinel; + let token; + let alUsd; + + beforeEach(async () => { + [deployer, sentinel, ...signers] = signers; + + token = await ERC20MockFactory.connect(deployer).deploy('Mock DAI', 'DAI', 18); + + alUsd = await AlUSDFactory.connect(deployer).deploy(); + }); + + context('when governance is the zero address', () => { + it('reverts', async () => { + expect( + AlchemistFactory.connect(deployer).deploy( + token.address, + alUsd.address, + ZERO_ADDRESS, + await sentinel.getAddress() + ) + ).revertedWith('Alchemist: governance address cannot be 0x0.'); + }); + }); + }); + + describe('update Alchemist addys and variables', () => { + let deployer; + let governance; + let newGovernance; + let rewards; + let sentinel; + let transmuter; + let token; + let alUsd; + let alchemist; + + beforeEach(async () => { + [ + deployer, + governance, + newGovernance, + rewards, + sentinel, + transmuter, + ...signers + ] = signers; + + token = await ERC20MockFactory.connect(deployer).deploy('Mock DAI', 'DAI', 18); + + alUsd = await AlUSDFactory.connect(deployer).deploy(); + + alchemist = await AlchemistFactory.connect(deployer).deploy( + token.address, + alUsd.address, + await governance.getAddress(), + await sentinel.getAddress() + ); + }); + + describe('set governance', () => { + context('when caller is not current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(deployer))); + + it('reverts', async () => { + expect( + alchemist.setPendingGovernance(await newGovernance.getAddress()) + ).revertedWith('Alchemist: only governance'); + }); + }); + + context('when caller is current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(governance))); + + it('reverts when setting governance to zero address', async () => { + expect(alchemist.setPendingGovernance(ZERO_ADDRESS)).revertedWith( + 'Alchemist: governance address cannot be 0x0.' + ); + }); + + it('updates rewards', async () => { + await alchemist.setRewards(await rewards.getAddress()); + expect(await alchemist.rewards()).equal(await rewards.getAddress()); + }); + }); + }); + + describe('set transmuter', () => { + context('when caller is not current governance', () => { + it('reverts', async () => { + expect( + alchemist.setTransmuter(await transmuter.getAddress()) + ).revertedWith('Alchemist: only governance'); + }); + }); + + context('when caller is current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(governance))); + + it('reverts when setting transmuter to zero address', async () => { + expect(alchemist.setTransmuter(ZERO_ADDRESS)).revertedWith( + 'Alchemist: transmuter address cannot be 0x0.' + ); + }); + + it('updates transmuter', async () => { + await alchemist.setTransmuter(await transmuter.getAddress()); + expect(await alchemist.transmuter()).equal(await transmuter.getAddress()); + }); + }); + }); + + describe('set rewards', () => { + context('when caller is not current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(deployer))); + + it('reverts', async () => { + expect(alchemist.setRewards(await rewards.getAddress())).revertedWith( + 'Alchemist: only governance' + ); + }); + }); + + context('when caller is current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(governance))); + + it('reverts when setting rewards to zero address', async () => { + expect(alchemist.setRewards(ZERO_ADDRESS)).revertedWith( + 'Alchemist: rewards address cannot be 0x0.' + ); + }); + + it('updates rewards', async () => { + await alchemist.setRewards(await rewards.getAddress()); + expect(await alchemist.rewards()).equal(await rewards.getAddress()); + }); + }); + }); + + describe('set peformance fee', () => { + context('when caller is not current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(deployer))); + + it('reverts', async () => { + expect(alchemist.setHarvestFee(1)).revertedWith( + 'Alchemist: only governance' + ); + }); + }); + + context('when caller is current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(governance))); + + it('reverts when performance fee greater than maximum', async () => { + const MAXIMUM_VALUE = await alchemist.PERCENT_RESOLUTION(); + expect(alchemist.setHarvestFee(MAXIMUM_VALUE.add(1))).revertedWith( + 'Alchemist: harvest fee above maximum' + ); + }); + + it('updates performance fee', async () => { + await alchemist.setHarvestFee(1); + expect(await alchemist.harvestFee()).equal(1); + }); + }); + }); + + describe('set collateralization limit', () => { + context('when caller is not current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(deployer))); + + it('reverts', async () => { + const collateralizationLimit = await alchemist.MINIMUM_COLLATERALIZATION_LIMIT(); + expect( + alchemist.setCollateralizationLimit(collateralizationLimit) + ).revertedWith('Alchemist: only governance'); + }); + }); + + context('when caller is current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(governance))); + + it('reverts when performance fee less than minimum', async () => { + const MINIMUM_LIMIT = await alchemist.MINIMUM_COLLATERALIZATION_LIMIT(); + expect( + alchemist.setCollateralizationLimit(MINIMUM_LIMIT.sub(1)) + ).revertedWith('Alchemist: collateralization limit below minimum.'); + }); + + it('reverts when performance fee greater than maximum', async () => { + const MAXIMUM_LIMIT = await alchemist.MAXIMUM_COLLATERALIZATION_LIMIT(); + expect( + alchemist.setCollateralizationLimit(MAXIMUM_LIMIT.add(1)) + ).revertedWith('Alchemist: collateralization limit above maximum'); + }); + + it('updates collateralization limit', async () => { + const collateralizationLimit = await alchemist.MINIMUM_COLLATERALIZATION_LIMIT(); + await alchemist.setCollateralizationLimit(collateralizationLimit); + // expect(await alchemist.collateralizationLimit()).containSubset([ + // collateralizationLimit, + // ]); + }); + }); + }); + }); + + describe('vault actions', () => { + let deployer; + let governance; + let sentinel; + let rewards; + let transmuter; + let minter; + let user; + let token; + let alUsd; + let alchemist; + let adapter; + let harvestFee = 1000; + let pctReso = 10000; + let transmuterContract; + + beforeEach(async () => { + [ + deployer, + governance, + sentinel, + rewards, + transmuter, + minter, + user, + ...signers + ] = signers; + + token = await ERC20MockFactory.connect(deployer).deploy('Mock DAI', 'DAI', 18); + + alUsd = await AlUSDFactory.connect(deployer).deploy(); + + alchemist = await AlchemistFactory.connect(deployer).deploy( + token.address, + alUsd.address, + await governance.getAddress(), + await sentinel.getAddress() + ); + + await alchemist.connect(governance).setTransmuter(await transmuter.getAddress()); + await alchemist.connect(governance).setRewards(await rewards.getAddress()); + await alchemist.connect(governance).setHarvestFee(harvestFee); + transmuterContract = await TransmuterFactory.connect(deployer).deploy( + alUsd.address, + token.address, + await governance.getAddress() + ); + await alchemist.connect(governance).setTransmuter(transmuterContract.address); + await transmuterContract.connect(governance).setWhitelist(alchemist.address, true); + await token.mint(await minter.getAddress(), parseEther('10000')); + await token.connect(minter).approve(alchemist.address, parseEther('10000')); + }); + + describe('migrate', () => { + beforeEach(async () => { + adapter = await VaultAdapterMockFactory.connect(deployer).deploy( + token.address + ); + + await alchemist.connect(governance).initialize(adapter.address); + }); + + context('when caller is not current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(deployer))); + + it('reverts', async () => { + expect(alchemist.migrate(adapter.address)).revertedWith( + 'Alchemist: only governance' + ); + }); + }); + + context('when caller is current governance', () => { + beforeEach(() => (alchemist = alchemist.connect(governance))); + + context('when adapter is zero address', async () => { + it('reverts', async () => { + expect(alchemist.migrate(ZERO_ADDRESS)).revertedWith( + 'Alchemist: active vault address cannot be 0x0.' + ); + }); + }); + + context('when adapter token mismatches', () => { + const tokenAddress = ethers.utils.getAddress( + '0xffffffffffffffffffffffffffffffffffffffff' + ); + + let invalidAdapter; + + beforeEach(async () => { + invalidAdapter = await VaultAdapterMockFactory.connect( + deployer + ).deploy(tokenAddress); + }); + + it('reverts', async () => { + expect(alchemist.migrate(invalidAdapter.address)).revertedWith( + 'Alchemist: token mismatch' + ); + }); + }); + + context('when conditions are met', () => { + beforeEach(async () => { + await alchemist.migrate(adapter.address); + }); + + it('increments the vault count', async () => { + expect(await alchemist.vaultCount()).equal(2); + }); + + it('sets the vaults adapter', async () => { + expect(await alchemist.getVaultAdapter(0)).equal(adapter.address); + }); + }); + }); + }); + + describe('recall funds', () => { + context('from the active vault', () => { + let adapter; + let controllerMock; + let vaultMock; + let depositAmt = parseEther('5000'); + let mintAmt = parseEther('1000'); + let recallAmt = parseEther('500'); + + beforeEach(async () => { + controllerMock = await YearnControllerMockFactory.connect( + deployer + ).deploy(); + vaultMock = await YearnVaultMockFactory.connect(deployer).deploy( + token.address, + controllerMock.address + ); + adapter = await YearnVaultAdapterFactory.connect(deployer).deploy( + vaultMock.address, + alchemist.address + ); + await token.mint(await deployer.getAddress(), parseEther('10000')); + await token.approve(vaultMock.address, parseEther('10000')); + await alchemist.connect(governance).initialize(adapter.address); + await alchemist.connect(minter).deposit(depositAmt); + await alchemist.flush(); + // need at least one other deposit in the vault to not get underflow errors + await vaultMock.connect(deployer).deposit(parseEther('100')); + }); + + it('reverts when not an emergency, not governance, and user does not have permission to recall funds from active vault', async () => { + expect(alchemist.connect(minter).recall(0, 0)).revertedWith( + 'Alchemist: not an emergency, not governance, and user does not have permission to recall funds from active vault' + ); + }); + + it('governance can recall some of the funds', async () => { + let beforeBal = await token + .connect(governance) + .balanceOf(alchemist.address); + await alchemist.connect(governance).recall(0, recallAmt); + let afterBal = await token + .connect(governance) + .balanceOf(alchemist.address); + expect(beforeBal).equal(0); + expect(afterBal).equal(recallAmt); + }); + + it('governance can recall all of the funds', async () => { + await alchemist.connect(governance).recallAll(0); + expect(await token.connect(governance).balanceOf(alchemist.address)).equal( + depositAmt + ); + }); + + describe('in an emergency', async () => { + it('anyone can recall funds', async () => { + await alchemist.connect(governance).setEmergencyExit(true); + await alchemist.connect(minter).recallAll(0); + expect( + await token.connect(governance).balanceOf(alchemist.address) + ).equal(depositAmt); + }); + + it('after some usage', async () => { + await alchemist.connect(minter).deposit(mintAmt); + await alchemist.connect(governance).flush(); + await token.mint(adapter.address, parseEther('500')); + await alchemist.connect(governance).setEmergencyExit(true); + await alchemist.connect(minter).recallAll(0); + expect( + await token.connect(governance).balanceOf(alchemist.address) + ).equal(depositAmt.add(mintAmt)); + }); + }); + }); + + context('from an inactive vault', () => { + let inactiveAdapter; + let activeAdapter; + let depositAmt = parseEther('5000'); + let recallAmt = parseEther('500'); + + beforeEach(async () => { + inactiveAdapter = await VaultAdapterMockFactory.connect(deployer).deploy( + token.address + ); + activeAdapter = await VaultAdapterMockFactory.connect(deployer).deploy( + token.address + ); + + await alchemist.connect(governance).initialize(inactiveAdapter.address); + await token.mint(await minter.getAddress(), depositAmt); + await token.connect(minter).approve(alchemist.address, depositAmt); + await alchemist.connect(minter).deposit(depositAmt); + await alchemist.connect(minter).flush(); + await alchemist.connect(governance).migrate(activeAdapter.address); + }); + + it('anyone can recall some of the funds to the contract', async () => { + await alchemist.connect(minter).recall(0, recallAmt); + expect(await token.balanceOf(alchemist.address)).equal(recallAmt); + }); + + it('anyone can recall all of the funds to the contract', async () => { + await alchemist.connect(minter).recallAll(0); + expect(await token.balanceOf(alchemist.address)).equal(depositAmt); + }); + + describe('in an emergency', async () => { + it('anyone can recall funds', async () => { + await alchemist.connect(governance).setEmergencyExit(true); + await alchemist.connect(minter).recallAll(0); + expect( + await token.connect(governance).balanceOf(alchemist.address) + ).equal(depositAmt); + }); + }); + }); + }); + + describe('flush funds', () => { + context('when the Alchemist is not initialized', () => { + it('reverts', async () => { + expect(alchemist.flush()).revertedWith('Alchemist: not initialized.'); + }); + }); + + context('when there is at least one vault to flush to', () => { + context('when there is one vault', () => { + let adapter; + let mintAmount = parseEther('5000'); + + beforeEach(async () => { + adapter = await VaultAdapterMockFactory.connect(deployer).deploy( + token.address + ); + }); + + beforeEach(async () => { + await token.mint(alchemist.address, mintAmount); + + await alchemist.connect(governance).initialize(adapter.address); + + await alchemist.flush(); + }); + + it('flushes funds to the vault', async () => { + expect(await token.balanceOf(adapter.address)).equal(mintAmount); + }); + }); + + context('when there are multiple vaults', () => { + let inactiveAdapter; + let activeAdapter; + let mintAmount = parseEther('5000'); + + beforeEach(async () => { + inactiveAdapter = await VaultAdapterMockFactory.connect( + deployer + ).deploy(token.address); + + activeAdapter = await VaultAdapterMockFactory.connect(deployer).deploy( + token.address + ); + + await token.mint(alchemist.address, mintAmount); + + await alchemist + .connect(governance) + .initialize(inactiveAdapter.address); + + await alchemist.connect(governance).migrate(activeAdapter.address); + + await alchemist.flush(); + }); + + it('flushes funds to the active vault', async () => { + expect(await token.balanceOf(activeAdapter.address)).equal(mintAmount); + }); + }); + }); + }); + + describe('deposit and withdraw tokens', () => { + let depositAmt = parseEther('5000'); + let mintAmt = parseEther('1000'); + let ceilingAmt = parseEther('10000'); + let collateralizationLimit = '2000000000000000000'; // this should be set in the deploy sequence + beforeEach(async () => { + adapter = await VaultAdapterMockFactory.connect(deployer).deploy( + token.address + ); + await alchemist.connect(governance).initialize(adapter.address); + await alchemist + .connect(governance) + .setCollateralizationLimit(collateralizationLimit); + await alUsd.connect(deployer).setWhitelist(alchemist.address, true); + await alUsd.connect(deployer).setCeiling(alchemist.address, ceilingAmt); + await token.mint(await minter.getAddress(), depositAmt); + await token + .connect(minter) + .approve(alchemist.address, parseEther('100000000')); + await alUsd + .connect(minter) + .approve(alchemist.address, parseEther('100000000')); + }); + + it('deposited amount is accounted for correctly', async () => { + // let address = await deployer.getAddress(); + await alchemist.connect(minter).deposit(depositAmt); + expect( + await alchemist + .connect(minter) + .getCdpTotalDeposited(await minter.getAddress()) + ).equal(depositAmt); + }); + + it('deposits token and then withdraws all', async () => { + let balBefore = await token.balanceOf(await minter.getAddress()); + await alchemist.connect(minter).deposit(depositAmt); + await alchemist.connect(minter).withdraw(depositAmt); + let balAfter = await token.balanceOf(await minter.getAddress()); + expect(balBefore).equal(balAfter); + }); + + it('reverts when withdrawing too much', async () => { + let overdraft = depositAmt.add(parseEther('1000')); + await alchemist.connect(minter).deposit(depositAmt); + expect(alchemist.connect(minter).withdraw(overdraft)).revertedWith( + 'ERC20: transfer amount exceeds balance' + ); + }); + + it('reverts when cdp is undercollateralized', async () => { + await alchemist.connect(minter).deposit(depositAmt); + await alchemist.connect(minter).mint(mintAmt); + expect(alchemist.connect(minter).withdraw(depositAmt)).revertedWith( + 'Action blocked: unhealthy collateralization ratio' + ); + }); + + it('deposits, mints, repays, and withdraws', async () => { + let balBefore = await token.balanceOf(await minter.getAddress()); + await alchemist.connect(minter).deposit(depositAmt); + await alchemist.connect(minter).mint(mintAmt); + await alchemist.connect(minter).repay(0, mintAmt); + await alchemist.connect(minter).withdraw(depositAmt); + let balAfter = await token.balanceOf(await minter.getAddress()); + expect(balBefore).equal(balAfter); + }); + + it('deposits 5000 DAI, mints 1000 alUSD, and withdraws 3000 DAI', async () => { + let withdrawAmt = depositAmt.sub(mintAmt.mul(2)); + await alchemist.connect(minter).deposit(depositAmt); + await alchemist.connect(minter).mint(mintAmt); + await alchemist.connect(minter).withdraw(withdrawAmt); + expect(await token.balanceOf(await minter.getAddress())).equal( + parseEther('13000') + ); + }); + + describe('flushActivator', async () => { + beforeEach(async () => { + await token.connect(deployer).approve(alchemist.address, parseEther('1')); + await token.mint(await deployer.getAddress(), parseEther('1')); + await token.mint(await minter.getAddress(), parseEther('100000')); + await alchemist.connect(deployer).deposit(parseEther('1')); + }); + + it('deposit() flushes funds if amount >= flushActivator', async () => { + let balBeforeWhale = await token.balanceOf(adapter.address); + await alchemist.connect(minter).deposit(parseEther('100000')); + let balAfterWhale = await token.balanceOf(adapter.address); + expect(balBeforeWhale).equal(0); + expect(balAfterWhale).equal(parseEther('100001')); + }); + + it('deposit() does not flush funds if amount < flushActivator', async () => { + let balBeforeWhale = await token.balanceOf(adapter.address); + await alchemist.connect(minter).deposit(parseEther('99999')); + let balAfterWhale = await token.balanceOf(adapter.address); + expect(balBeforeWhale).equal(0); + expect(balAfterWhale).equal(0); + }); + + it('withdraw() flushes funds if amount >= flushActivator', async () => { + await alchemist.connect(minter).deposit(parseEther('50000')); + await alchemist.connect(minter).deposit(parseEther('50000')); + let balBeforeWhaleWithdraw = await token.balanceOf(adapter.address); + await alchemist.connect(minter).withdraw(parseEther('100000')); + let balAfterWhaleWithdraw = await token.balanceOf(adapter.address); + expect(balBeforeWhaleWithdraw).equal(0); + expect(balAfterWhaleWithdraw).equal(parseEther('1')); + }); + + it('withdraw() does not flush funds if amount < flushActivator', async () => { + await alchemist.connect(minter).deposit(parseEther('50000')); + await alchemist.connect(minter).deposit(parseEther('50000')); + let balBeforeWhaleWithdraw = await token.balanceOf(adapter.address); + await alchemist.connect(minter).withdraw(parseEther('99999')); + let balAfterWhaleWithdraw = await token.balanceOf(adapter.address); + expect(balBeforeWhaleWithdraw).equal(0); + expect(balAfterWhaleWithdraw).equal(0); + }); + }); + }); + + describe('repay and liquidate tokens', () => { + let depositAmt = parseEther('5000'); + let mintAmt = parseEther('1000'); + let ceilingAmt = parseEther('10000'); + let collateralizationLimit = '2000000000000000000'; // this should be set in the deploy sequence + beforeEach(async () => { + adapter = await VaultAdapterMockFactory.connect(deployer).deploy( + token.address + ); + await alchemist.connect(governance).initialize(adapter.address); + await alchemist + .connect(governance) + .setCollateralizationLimit(collateralizationLimit); + await alUsd.connect(deployer).setWhitelist(alchemist.address, true); + await alUsd.connect(deployer).setCeiling(alchemist.address, ceilingAmt); + await token.mint(await minter.getAddress(), ceilingAmt); + await token.connect(minter).approve(alchemist.address, ceilingAmt); + await alUsd + .connect(minter) + .approve(alchemist.address, parseEther('100000000')); + await token.connect(minter).approve(transmuterContract.address, ceilingAmt); + await alUsd.connect(minter).approve(transmuterContract.address, depositAmt); + }); + it('repay with dai reverts when nothing is minted and transmuter has no alUsd deposits', async () => { + await alchemist.connect(minter).deposit(depositAmt.sub(parseEther('1000'))); + expect(alchemist.connect(minter).repay(mintAmt, 0)).revertedWith( + 'SafeMath: subtraction overflow' + ); + }); + it('liquidate max amount possible if trying to liquidate too much', async () => { + let liqAmt = depositAmt; + await alchemist.connect(minter).deposit(depositAmt); + await alchemist.connect(minter).mint(mintAmt); + await transmuterContract.connect(minter).stake(mintAmt); + await alchemist.connect(minter).liquidate(liqAmt); + const transBal = await token.balanceOf(transmuterContract.address); + expect(transBal).equal(mintAmt); + }); + it('liquidates funds from vault if not enough in the buffer', async () => { + let liqAmt = parseEther('600'); + await alchemist.connect(minter).deposit(depositAmt); + await alchemist.connect(governance).flush(); + await alchemist.connect(minter).deposit(mintAmt.div(2)); + await alchemist.connect(minter).mint(mintAmt); + await transmuterContract.connect(minter).stake(mintAmt); + await alchemist.connect(minter).liquidate(liqAmt); + const alchemistTokenBalPost = await token.balanceOf(alchemist.address); + const transmuterEndingTokenBal = await token.balanceOf( + transmuterContract.address + ); + expect(alchemistTokenBalPost).equal(0); + expect(transmuterEndingTokenBal).equal(liqAmt); + }); + it('liquidates the minimum necessary from the alchemist buffer', async () => { + let dep2Amt = parseEther('500'); + let liqAmt = parseEther('200'); + await alchemist.connect(minter).deposit(parseEther('2000')); + await alchemist.connect(governance).flush(); + await alchemist.connect(minter).deposit(dep2Amt); + await alchemist.connect(minter).mint(parseEther('1000')); + await transmuterContract.connect(minter).stake(parseEther('1000')); + await alchemist.connect(minter).liquidate(liqAmt); + const alchemistTokenBalPost = await token.balanceOf(alchemist.address); + + const transmuterEndingTokenBal = await token.balanceOf( + transmuterContract.address + ); + expect(alchemistTokenBalPost).equal(dep2Amt.sub(liqAmt)); + expect(transmuterEndingTokenBal).equal(liqAmt); + }); + it('deposits, mints alUsd, repays, and has no outstanding debt', async () => { + await alchemist.connect(minter).deposit(depositAmt.sub(parseEther('1000'))); + await alchemist.connect(minter).mint(mintAmt); + await transmuterContract.connect(minter).stake(mintAmt); + await alchemist.connect(minter).repay(mintAmt, 0); + expect( + await alchemist.connect(minter).getCdpTotalDebt(await minter.getAddress()) + ).equal(0); + }); + it('deposits, mints, repays, and has no outstanding debt', async () => { + await alchemist.connect(minter).deposit(depositAmt); + await alchemist.connect(minter).mint(mintAmt); + await alchemist.connect(minter).repay(0, mintAmt); + expect( + await alchemist.connect(minter).getCdpTotalDebt(await minter.getAddress()) + ).equal(0); + }); + it('deposits, mints alUsd, repays with alUsd and DAI, and has no outstanding debt', async () => { + await alchemist.connect(minter).deposit(depositAmt.sub(parseEther('1000'))); + await alchemist.connect(minter).mint(mintAmt); + await transmuterContract.connect(minter).stake(parseEther('500')); + await alchemist.connect(minter).repay(parseEther('500'), parseEther('500')); + expect( + await alchemist.connect(minter).getCdpTotalDebt(await minter.getAddress()) + ).equal(0); + }); + + it('deposits and liquidates DAI', async () => { + await alchemist.connect(minter).deposit(depositAmt); + await alchemist.connect(minter).mint(mintAmt); + await transmuterContract.connect(minter).stake(mintAmt); + await alchemist.connect(minter).liquidate(mintAmt); + expect( + await alchemist + .connect(minter) + .getCdpTotalDeposited(await minter.getAddress()) + ).equal(depositAmt.sub(mintAmt)); + }); + }); + + // describe('mint', () => { + // let depositAmt = parseEther('5000'); + // let mintAmt = parseEther('1000'); + // let ceilingAmt = parseEther('1000'); + + // beforeEach(async () => { + // adapter = await VaultAdapterMockFactory.connect(deployer).deploy( + // token.address + // ); + + // await alchemist.connect(governance).initialize(adapter.address); + + // await alUsd.connect(deployer).setCeiling(alchemist.address, ceilingAmt); + // await token.mint(await minter.getAddress(), depositAmt); + // await token.connect(minter).approve(alchemist.address, depositAmt); + // await alUsd.connect(deployer).setWhitelist(alchemist.address, true); + // }); + + // // it('reverts if the Alchemist is not whitelisted', async () => { + // // await alchemist.connect(minter).deposit(depositAmt); + // // expect(alchemist.connect(minter).mint(mintAmt)).revertedWith( + // // 'AlUSD is not whitelisted' + // // ); + // // }); + + // context('is whiltelisted', () => { + // // beforeEach(async () => { + // // await alUsd.connect(deployer).setWhitelist(alchemist.address, true); + // // }); + + // it('reverts if the Alchemist is blacklisted', async () => { + // // await alUsd.connect(deployer).setWhitelist(alchemist.address, true); + // await alUsd.connect(deployer).setBlacklist(alchemist.address); + // await alchemist.connect(minter).deposit(depositAmt); + // expect(alchemist.connect(minter).mint(mintAmt)).revertedWith( + // 'AlUSD is blacklisted' + // ); + // }); + + // it('reverts when trying to mint too much', async () => { + // expect(alchemist.connect(minter).mint(parseEther('2000'))).revertedWith( + // 'Loan-to-value ratio breached' + // ); + // }); + + // it('reverts if the ceiling was breached', async () => { + // let lowCeilingAmt = parseEther('100'); + // await alUsd.connect(deployer).setCeiling(alchemist.address, lowCeilingAmt); + // await alchemist.connect(minter).deposit(depositAmt); + // expect(alchemist.connect(minter).mint(mintAmt)).revertedWith( + // "AlUSD's ceiling was breached" + // ); + // }); + + // it('mints successfully to depositor', async () => { + // let balBefore = await token.balanceOf(await minter.getAddress()); + // await alchemist.connect(minter).deposit(depositAmt); + // await alchemist.connect(minter).mint(mintAmt); + // let balAfter = await token.balanceOf(await minter.getAddress()); + + // expect(balAfter).equal(balBefore.sub(depositAmt)); + // expect(await alUsd.balanceOf(await minter.getAddress())).equal(mintAmt); + // }); + + // describe('flushActivator', async () => { + // beforeEach(async () => { + // await alUsd + // .connect(deployer) + // .setCeiling(alchemist.address, parseEther('200000')); + // await token.mint(await minter.getAddress(), parseEther('200000')); + // await token + // .connect(minter) + // .approve(alchemist.address, parseEther('200000')); + // }); + + // it('mint() flushes funds if amount >= flushActivator', async () => { + // await alchemist.connect(minter).deposit(parseEther('50000')); + // await alchemist.connect(minter).deposit(parseEther('50000')); + // await alchemist.connect(minter).deposit(parseEther('50000')); + // await alchemist.connect(minter).deposit(parseEther('50000')); + // let balBeforeWhale = await token.balanceOf(adapter.address); + // await alchemist.connect(minter).mint(parseEther('100000')); + // let balAfterWhale = await token.balanceOf(adapter.address); + // expect(balBeforeWhale).equal(0); + // expect(balAfterWhale).equal(parseEther('200000')); + // }); + + // it('mint() does not flush funds if amount < flushActivator', async () => { + // await alchemist.connect(minter).deposit(parseEther('50000')); + // await alchemist.connect(minter).deposit(parseEther('50000')); + // await alchemist.connect(minter).deposit(parseEther('50000')); + // await alchemist.connect(minter).deposit(parseEther('50000')); + // let balBeforeWhale = await token.balanceOf(adapter.address); + // await alchemist.connect(minter).mint(parseEther('99999')); + // let balAfterWhale = await token.balanceOf(adapter.address); + // expect(balBeforeWhale).equal(0); + // expect(balAfterWhale).equal(0); + // }); + // }); + // }); + // }); + + describe('harvest', () => { + let depositAmt = parseEther('5000'); + let mintAmt = parseEther('1000'); + let stakeAmt = mintAmt.div(2); + let ceilingAmt = parseEther('10000'); + let yieldAmt = parseEther('100'); + + beforeEach(async () => { + adapter = await VaultAdapterMockFactory.connect(deployer).deploy( + token.address + ); + + await alUsd.connect(deployer).setWhitelist(alchemist.address, true); + await alchemist.connect(governance).initialize(adapter.address); + await alUsd.connect(deployer).setCeiling(alchemist.address, ceilingAmt); + await token.mint(await user.getAddress(), depositAmt); + await token.connect(user).approve(alchemist.address, depositAmt); + await alUsd.connect(user).approve(transmuterContract.address, depositAmt); + await alchemist.connect(user).deposit(depositAmt); + await alchemist.connect(user).mint(mintAmt); + await transmuterContract.connect(user).stake(stakeAmt); + await alchemist.flush(); + }); + + it('harvests yield from the vault', async () => { + await token.mint(adapter.address, yieldAmt); + await alchemist.harvest(0); + let transmuterBal = await token.balanceOf(transmuterContract.address); + expect(transmuterBal).equal(yieldAmt.sub(yieldAmt.div(pctReso / harvestFee))); + let vaultBal = await token.balanceOf(adapter.address); + expect(vaultBal).equal(depositAmt); + }); + + it('sends the harvest fee to the rewards address', async () => { + await token.mint(adapter.address, yieldAmt); + await alchemist.harvest(0); + let rewardsBal = await token.balanceOf(await rewards.getAddress()); + expect(rewardsBal).equal(yieldAmt.mul(100).div(harvestFee)); + }); + + it('does not update any balances if there is nothing to harvest', async () => { + let initTransBal = await token.balanceOf(transmuterContract.address); + let initRewardsBal = await token.balanceOf(await rewards.getAddress()); + await alchemist.harvest(0); + let endTransBal = await token.balanceOf(transmuterContract.address); + let endRewardsBal = await token.balanceOf(await rewards.getAddress()); + expect(initTransBal).equal(endTransBal); + expect(initRewardsBal).equal(endRewardsBal); + }); + }); + }); +}); diff --git a/test/v3/Transmuter.test.js b/test/v3/Transmuter.test.js new file mode 100644 index 00000000..f0963166 --- /dev/null +++ b/test/v3/Transmuter.test.js @@ -0,0 +1,623 @@ +const chai = require('chai'); +const { solidity } = require('ethereum-waffle'); +const { ethers } = require('hardhat'); +const { BigNumber, utils } = require('ethers'); +const { parseEther } = require('ethers/lib/utils'); +const { MAXIMUM_U256, mineBlocks } = require('../helpers/utils'); + +chai.use(solidity); + +const { expect } = chai; + +let AlchemistFactory; +let TransmuterFactory; +let ERC20MockFactory; +let AlUSDFactory; +let VaultAdapterMockFactory; + +describe('Transmuter', () => { + let deployer; + let depositor; + let alchemist; + let governance; + let minter; + let rewards; + let sentinel; + let user; + let mockAlchemist; + let token; + let transmuter; + let adapter; + let alUsd; + let harvestFee = 1000; + let ceilingAmt = utils.parseEther('10000000'); + let collateralizationLimit = '2000000000000000000'; + let mockAlchemistAddress; + let preTestTotalAlUSDSupply; + + before(async () => { + TransmuterFactory = await ethers.getContractFactory('Transmuter'); + ERC20MockFactory = await ethers.getContractFactory('ERC20Mock'); + AlUSDFactory = await ethers.getContractFactory('AlToken'); + AlchemistFactory = await ethers.getContractFactory('Alchemist'); + VaultAdapterMockFactory = await ethers.getContractFactory('VaultAdapterMock'); + }); + + beforeEach(async () => { + [ + deployer, + rewards, + depositor, + sentinel, + minter, + governance, + mockAlchemist, + user + ] = await ethers.getSigners(); + + token = await ERC20MockFactory.connect(deployer).deploy('Mock DAI', 'DAI', 18); + + alUsd = await AlUSDFactory.connect(deployer).deploy(); + + mockAlchemistAddress = await mockAlchemist.getAddress(); + + alchemist = await AlchemistFactory.connect(deployer).deploy( + token.address, + alUsd.address, + await governance.getAddress(), + await sentinel.getAddress() + ); + transmuter = await TransmuterFactory.connect(deployer).deploy( + alUsd.address, + token.address, + await governance.getAddress() + ); + await transmuter.connect(governance).setTransmutationPeriod(40320); + await alchemist.connect(governance).setTransmuter(transmuter.address); + await alchemist.connect(governance).setRewards(await rewards.getAddress()); + await alchemist.connect(governance).setHarvestFee(harvestFee); + await transmuter.connect(governance).setWhitelist(mockAlchemistAddress, true); + + adapter = await VaultAdapterMockFactory.connect(deployer).deploy(token.address); + await alchemist.connect(governance).initialize(adapter.address); + await alchemist.connect(governance).setCollateralizationLimit(collateralizationLimit); + await alUsd.connect(deployer).setWhitelist(alchemist.address, true); + await alUsd.connect(deployer).setCeiling(alchemist.address, ceilingAmt); + await token.mint(mockAlchemistAddress, utils.parseEther('10000')); + await token.connect(mockAlchemist).approve(transmuter.address, MAXIMUM_U256); + + await token.mint(await depositor.getAddress(), utils.parseEther('20000')); + await token.mint(await minter.getAddress(), utils.parseEther('20000')); + await token.connect(depositor).approve(transmuter.address, MAXIMUM_U256); + await alUsd.connect(depositor).approve(transmuter.address, MAXIMUM_U256); + await token.connect(depositor).approve(alchemist.address, MAXIMUM_U256); + await alUsd.connect(depositor).approve(alchemist.address, MAXIMUM_U256); + await token.connect(minter).approve(transmuter.address, MAXIMUM_U256); + await alUsd.connect(minter).approve(transmuter.address, MAXIMUM_U256); + await token.connect(minter).approve(alchemist.address, MAXIMUM_U256); + await alUsd.connect(minter).approve(alchemist.address, MAXIMUM_U256); + + await alchemist.connect(depositor).deposit(utils.parseEther('10000')); + await alchemist.connect(depositor).mint(utils.parseEther('5000')); + + await alchemist.connect(minter).deposit(utils.parseEther('10000')); + await alchemist.connect(minter).mint(utils.parseEther('5000')); + + transmuter = transmuter.connect(depositor); + + preTestTotalAlUSDSupply = await alUsd.totalSupply(); + }); + + describe('stake()', () => { + it('stakes 1000 alUsd and reads the correct amount', async () => { + await transmuter.stake(1000); + expect(await transmuter.depositedAlTokens(await depositor.getAddress())).equal( + 1000 + ); + }); + + it('stakes 1000 alUsd two times and reads the correct amount', async () => { + await transmuter.stake(1000); + await transmuter.stake(1000); + expect(await transmuter.depositedAlTokens(await depositor.getAddress())).equal( + 2000 + ); + }); + }); + + describe('unstake()', () => { + it('reverts on depositing and then unstaking balance greater than deposit', async () => { + await transmuter.stake(utils.parseEther('1000')); + expect(transmuter.unstake(utils.parseEther('2000'))).revertedWith( + 'Transmuter: unstake amount exceeds deposited amount' + ); + }); + + it('deposits and unstakes 1000 alUSD', async () => { + await transmuter.stake(utils.parseEther('1000')); + await transmuter.unstake(utils.parseEther('1000')); + expect(await transmuter.depositedAlTokens(await depositor.getAddress())).equal(0); + }); + + it('deposits 1000 alUSD and unstaked 500 alUSd', async () => { + await transmuter.stake(utils.parseEther('1000')); + await transmuter.unstake(utils.parseEther('500')); + expect(await transmuter.depositedAlTokens(await depositor.getAddress())).equal( + utils.parseEther('500') + ); + }); + }); + + describe('distributes correct amount', () => { + let distributeAmt = utils.parseEther('1000'); + let stakeAmt = utils.parseEther('1000'); + let transmutationPeriod = 20; + + beforeEach(async () => { + await transmuter.connect(governance).setTransmutationPeriod(transmutationPeriod); + await token.mint(await minter.getAddress(), utils.parseEther('20000')); + await token.connect(minter).approve(transmuter.address, MAXIMUM_U256); + await alUsd.connect(minter).approve(transmuter.address, MAXIMUM_U256); + await token.connect(minter).approve(alchemist.address, MAXIMUM_U256); + await alUsd.connect(minter).approve(alchemist.address, MAXIMUM_U256); + await alchemist.connect(minter).deposit(utils.parseEther('10000')); + await alchemist.connect(minter).mint(utils.parseEther('5000')); + await token.mint(await rewards.getAddress(), utils.parseEther('20000')); + await token.connect(rewards).approve(transmuter.address, MAXIMUM_U256); + await alUsd.connect(rewards).approve(transmuter.address, MAXIMUM_U256); + await token.connect(rewards).approve(alchemist.address, MAXIMUM_U256); + await alUsd.connect(rewards).approve(alchemist.address, MAXIMUM_U256); + await alchemist.connect(rewards).deposit(utils.parseEther('10000')); + await alchemist.connect(rewards).mint(utils.parseEther('5000')); + }); + + it('deposits 100000 alUSD, distributes 1000 DAI, and the correct amount of tokens are distributed to depositor', async () => { + let numBlocks = 5; + await transmuter.connect(depositor).stake(stakeAmt); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await mineBlocks(ethers.provider, numBlocks); + let userInfo = await transmuter.userInfo(await depositor.getAddress()); + // pendingdivs should be (distributeAmt * (numBlocks / transmutationPeriod)) + expect(userInfo.pendingdivs).equal(distributeAmt.div(4)); + }); + + it('two people deposit equal amounts and recieve equal amounts in distribution', async () => { + await transmuter.connect(depositor).stake(utils.parseEther('1000')); + await transmuter.connect(minter).stake(utils.parseEther('1000')); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await mineBlocks(ethers.provider, 10); + let userInfo1 = await transmuter.userInfo(await depositor.getAddress()); + let userInfo2 = await transmuter.userInfo(await minter.getAddress()); + expect(userInfo1.pendingdivs).gt(0); + expect(userInfo1.pendingdivs).equal(userInfo2.pendingdivs); + }); + + it('deposits of 500, 250, and 250 from three people and distribution is correct', async () => { + await transmuter.connect(depositor).stake(utils.parseEther('500')); + await transmuter.connect(minter).stake(utils.parseEther('250')); + await transmuter.connect(rewards).stake(utils.parseEther('250')); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await mineBlocks(ethers.provider, 10); + let userInfo1 = await transmuter.userInfo(await depositor.getAddress()); + let userInfo2 = await transmuter.userInfo(await minter.getAddress()); + let userInfo3 = await transmuter.userInfo(await rewards.getAddress()); + let user2 = userInfo2.pendingdivs; + let user3 = userInfo3.pendingdivs; + let sumOfTwoUsers = user2.add(user3); + expect(userInfo1.pendingdivs).gt(0); + expect(sumOfTwoUsers).equal(userInfo1.pendingdivs); + }); + }); + + describe('transmute() claim() transmuteAndClaim()', () => { + let distributeAmt = utils.parseEther('500'); + let transmutedAmt = BigNumber.from('12400793650793600'); + + it('transmutes the correct amount', async () => { + await transmuter.stake(utils.parseEther('1000')); + await mineBlocks(ethers.provider, 10); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await transmuter.transmute(); + let userInfo = await transmuter.userInfo(await depositor.getAddress()); + expect(userInfo.realised).equal(transmutedAmt); + }); + + it('burns the supply of alUSD on transmute()', async () => { + await transmuter.stake(utils.parseEther('1000')); + await mineBlocks(ethers.provider, 10); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await transmuter.transmute(); + let alUSDTokenSupply = await alUsd.totalSupply(); + expect(alUSDTokenSupply).equal(preTestTotalAlUSDSupply.sub(transmutedAmt)); + }); + + it('moves DAI from pendingdivs to inbucket upon staking more', async () => { + await transmuter.stake(utils.parseEther('1000')); + await mineBlocks(ethers.provider, 10); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await transmuter.stake(utils.parseEther('100')); + let userInfo = await transmuter.userInfo(await depositor.getAddress()); + expect(userInfo.inbucket).equal(transmutedAmt); + }); + + it('transmutes and claims using transmute() and then claim()', async () => { + await transmuter.stake(utils.parseEther('1000')); + await mineBlocks(ethers.provider, 10); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + let tokenBalanceBefore = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + await transmuter.transmute(); + await transmuter.claim(); + let tokenBalanceAfter = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + expect(tokenBalanceAfter).equal(tokenBalanceBefore.add(transmutedAmt)); + }); + + it('transmutes and claims using transmuteAndClaim()', async () => { + await transmuter.stake(utils.parseEther('1000')); + await mineBlocks(ethers.provider, 10); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + let tokenBalanceBefore = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + await transmuter.transmuteAndClaim(); + let tokenBalanceAfter = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + expect(tokenBalanceAfter).equal(tokenBalanceBefore.add(transmutedAmt)); + }); + + it('transmutes the full buffer if a complete phase has passed', async () => { + await transmuter.stake(utils.parseEther('1000')); + await transmuter.connect(governance).setTransmutationPeriod(10); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await mineBlocks(ethers.provider, 11); + let tokenBalanceBefore = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + await transmuter.connect(depositor).transmuteAndClaim(); + let tokenBalanceAfter = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + expect(tokenBalanceAfter).equal(tokenBalanceBefore.add(distributeAmt)); + }); + + it('transmutes the staked amount and distributes overflow if a bucket overflows', async () => { + // 1) DEPOSITOR stakes 100 dai + // 2) distribution of 90 dai, let transmutation period pass + // DEPOSITOR gets 90 dai + // 3) MINTER stakes 200 dai + // 4) distribution of 60 dai, let transmutation period pass + // DEPOSITOR gets 20 dai, MINTER gets 40 dai + // 5) USER stakes 200 dai (to distribute allocations) + // 6) transmute DEPOSITOR, bucket overflows by 10 dai + // MINTER gets 5 dai, USER gets 5 dai + let distributeAmt0 = utils.parseEther('90'); + let distributeAmt1 = utils.parseEther('60'); + let depStakeAmt0 = utils.parseEther('100'); + let depStakeAmt1 = utils.parseEther('200'); + await transmuter.connect(governance).setTransmutationPeriod(10); + await token.connect(minter).approve(transmuter.address, MAXIMUM_U256); + await alUsd.connect(minter).approve(transmuter.address, MAXIMUM_U256); + await alUsd.connect(user).approve(transmuter.address, MAXIMUM_U256); + await token.connect(minter).approve(alchemist.address, MAXIMUM_U256); + await token.connect(user).approve(alchemist.address, MAXIMUM_U256); + await alUsd.connect(minter).approve(alchemist.address, MAXIMUM_U256); + await alUsd.connect(user).approve(alchemist.address, MAXIMUM_U256); + await token.mint(await minter.getAddress(), utils.parseEther('20000')); + await alchemist.connect(minter).deposit(utils.parseEther('10000')); + await alchemist.connect(minter).mint(utils.parseEther('5000')); + await token.mint(await user.getAddress(), utils.parseEther('20000')); + await alchemist.connect(user).deposit(utils.parseEther('10000')); + await alchemist.connect(user).mint(utils.parseEther('5000')); + + // user 1 deposit + await transmuter.connect(depositor).stake(depStakeAmt0); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt0); + await mineBlocks(ethers.provider, 10); + + // user 2 deposit + await transmuter.connect(minter).stake(depStakeAmt1); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt1); + await mineBlocks(ethers.provider, 10); + + await transmuter.connect(user).stake(depStakeAmt1); + + let minterInfo = await transmuter.userInfo(await minter.getAddress()); + let minterBucketBefore = minterInfo.inbucket; + await transmuter.connect(depositor).transmuteAndClaim(); + minterInfo = await transmuter.userInfo(await minter.getAddress()); + let userInfo = await transmuter.userInfo(await user.getAddress()); + + let minterBucketAfter = minterInfo.inbucket; + expect(minterBucketAfter).equal(minterBucketBefore.add(parseEther('5'))); + expect(userInfo.inbucket).equal(parseEther('5')); + }); + }); + + describe('transmuteClaimAndWithdraw()', () => { + let distributeAmt = utils.parseEther('500'); + let transmutedAmt = BigNumber.from('6200396825396800'); + let alUsdBalanceBefore; + let tokenBalanceBefore; + + beforeEach(async () => { + tokenBalanceBefore = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + alUsdBalanceBefore = await alUsd + .connect(depositor) + .balanceOf(await depositor.getAddress()); + await transmuter.stake(utils.parseEther('1000')); + await transmuter.connect(minter).stake(utils.parseEther('1000')); + await mineBlocks(ethers.provider, 10); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await transmuter.transmuteClaimAndWithdraw(); + }); + + it('has a staking balance of 0 alUSD after transmuteClaimAndWithdraw()', async () => { + let userInfo = await transmuter.userInfo(await depositor.getAddress()); + expect(userInfo.depositedAl).equal(0); + expect(await transmuter.depositedAlTokens(await depositor.getAddress())).equal(0); + }); + + it('returns the amount of alUSD staked less the transmuted amount', async () => { + let alUsdBalanceAfter = await alUsd + .connect(depositor) + .balanceOf(await depositor.getAddress()); + expect(alUsdBalanceAfter).equal(alUsdBalanceBefore.sub(transmutedAmt)); + }); + + it('burns the correct amount of transmuted alUSD using transmuteClaimAndWithdraw()', async () => { + let alUSDTokenSupply = await alUsd.totalSupply(); + expect(alUSDTokenSupply).equal(preTestTotalAlUSDSupply.sub(transmutedAmt)); + }); + + it('successfully sends DAI to owner using transmuteClaimAndWithdraw()', async () => { + let tokenBalanceAfter = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + expect(tokenBalanceAfter).equal(tokenBalanceBefore.add(transmutedAmt)); + }); + }); + + describe('exit()', () => { + let distributeAmt = utils.parseEther('500'); + let transmutedAmt = BigNumber.from('6200396825396800'); + let alUsdBalanceBefore; + let tokenBalanceBefore; + + beforeEach(async () => { + tokenBalanceBefore = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + alUsdBalanceBefore = await alUsd + .connect(depositor) + .balanceOf(await depositor.getAddress()); + await transmuter.stake(utils.parseEther('1000')); + await transmuter.connect(minter).stake(utils.parseEther('1000')); + await mineBlocks(ethers.provider, 10); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await transmuter.exit(); + }); + + it('transmutes and then withdraws alUSD from staking', async () => { + let alUsdBalanceAfter = await alUsd + .connect(depositor) + .balanceOf(await depositor.getAddress()); + expect(alUsdBalanceAfter).equal(alUsdBalanceBefore.sub(transmutedAmt)); + }); + + it('transmutes and claimable DAI moves to realised value', async () => { + let userInfo = await transmuter.userInfo(await depositor.getAddress()); + expect(userInfo.realised).equal(transmutedAmt); + }); + + it('does not claim the realized tokens', async () => { + let tokenBalanceAfter = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + expect(tokenBalanceAfter).equal(tokenBalanceBefore); + }); + }); + + describe('forceTransmute()', () => { + let distributeAmt = utils.parseEther('5000'); + + beforeEach(async () => { + transmuter.connect(governance).setTransmutationPeriod(10); + await token.mint(await minter.getAddress(), utils.parseEther('20000')); + await token.connect(minter).approve(transmuter.address, MAXIMUM_U256); + await alUsd.connect(minter).approve(transmuter.address, MAXIMUM_U256); + await token.connect(minter).approve(alchemist.address, MAXIMUM_U256); + await alUsd.connect(minter).approve(alchemist.address, MAXIMUM_U256); + await alchemist.connect(minter).deposit(utils.parseEther('10000')); + await alchemist.connect(minter).mint(utils.parseEther('5000')); + await transmuter.connect(depositor).stake(utils.parseEther('.01')); + }); + + it("User 'depositor' has alUSD overfilled, user 'minter' force transmutes user 'depositor' and user 'depositor' has DAI sent to his address", async () => { + await transmuter.connect(minter).stake(utils.parseEther('10')); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await mineBlocks(ethers.provider, 10); + let tokenBalanceBefore = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + await transmuter.connect(minter).forceTransmute(await depositor.getAddress()); + let tokenBalanceAfter = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + expect(tokenBalanceBefore).equal(tokenBalanceAfter.sub(utils.parseEther('0.01'))); + }); + + it("User 'depositor' has alUSD overfilled, user 'minter' force transmutes user 'depositor' and user 'minter' overflow added inbucket", async () => { + await transmuter.connect(minter).stake(utils.parseEther('10')); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await mineBlocks(ethers.provider, 10); + await transmuter.connect(minter).forceTransmute(await depositor.getAddress()); + let userInfo = await transmuter + .connect(minter) + .userInfo(await minter.getAddress()); + // TODO calculate the expected value + expect(userInfo.inbucket).equal('4999989999999999999999'); + }); + + it('you can force transmute yourself', async () => { + await transmuter.connect(minter).stake(utils.parseEther('1')); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await mineBlocks(ethers.provider, 10); + let tokenBalanceBefore = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + await transmuter.connect(depositor).forceTransmute(await depositor.getAddress()); + let tokenBalanceAfter = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + expect(tokenBalanceBefore).equal(tokenBalanceAfter.sub(utils.parseEther('0.01'))); + }); + + it('you can force transmute yourself even when you are the only one in the transmuter', async () => { + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, distributeAmt); + await mineBlocks(ethers.provider, 10); + let tokenBalanceBefore = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + await transmuter.connect(depositor).forceTransmute(await depositor.getAddress()); + let tokenBalanceAfter = await token + .connect(depositor) + .balanceOf(await depositor.getAddress()); + expect(tokenBalanceBefore).equal(tokenBalanceAfter.sub(utils.parseEther('0.01'))); + }); + + it('reverts when you are not overfilled', async () => { + await transmuter.connect(minter).stake(utils.parseEther('1000')); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, utils.parseEther('1000')); + expect( + transmuter.connect(minter).forceTransmute(await depositor.getAddress()) + ).revertedWith('Transmuter: !overflow'); + }); + }); + //not sure what this is actually testing.... REEEE + describe('Multiple Users displays all overfilled users', () => { + it('returns userInfo', async () => { + await transmuter.stake(utils.parseEther('1000')); + await transmuter.connect(minter).stake(utils.parseEther('1000')); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, utils.parseEther('5000')); + let multipleUsers = await transmuter.getMultipleUserInfo(0, 1); + let userList = multipleUsers.theUserData; + expect(userList.length).equal(2); + }); + }); + + describe('distribute()', () => { + let transmutationPeriod = 20; + + beforeEach(async () => { + await transmuter.connect(governance).setTransmutationPeriod(transmutationPeriod); + }); + + it('must be whitelisted to call distribute', async () => { + await transmuter.connect(depositor).stake(utils.parseEther('1000')); + expect( + transmuter + .connect(depositor) + .distribute(alchemist.address, utils.parseEther('1000')) + ).revertedWith('Transmuter: !whitelisted'); + }); + + it('increases buffer size, but does not immediately increase allocations', async () => { + await transmuter.connect(depositor).stake(utils.parseEther('1000')); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, utils.parseEther('1000')); + let userInfo = await transmuter.userInfo(await depositor.getAddress()); + let bufferInfo = await transmuter.bufferInfo(); + + expect(bufferInfo._buffer).equal(utils.parseEther('1000')); + expect(bufferInfo._deltaBlocks).equal(0); + expect(bufferInfo._toDistribute).equal(0); + expect(userInfo.pendingdivs).equal(0); + expect(userInfo.depositedAl).equal(utils.parseEther('1000')); + expect(userInfo.inbucket).equal(0); + expect(userInfo.realised).equal(0); + }); + + describe('userInfo()', async () => { + it('distribute increases allocations if the buffer is already > 0', async () => { + let blocksMined = 10; + let stakeAmt = utils.parseEther('1000'); + await transmuter.connect(depositor).stake(stakeAmt); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, utils.parseEther('1000')); + await mineBlocks(ethers.provider, blocksMined); + let userInfo = await transmuter.userInfo(await depositor.getAddress()); + let bufferInfo = await transmuter.bufferInfo(); + + // 2 = transmutationPeriod / blocksMined + expect(bufferInfo._buffer).equal(stakeAmt); + expect(userInfo.pendingdivs).equal(stakeAmt.div(2)); + expect(userInfo.depositedAl).equal(stakeAmt); + expect(userInfo.inbucket).equal(0); + expect(userInfo.realised).equal(0); + }); + + it('increases buffer size, and userInfo() shows the correct state without an extra nudge', async () => { + let stakeAmt = utils.parseEther('1000'); + await transmuter.connect(depositor).stake(stakeAmt); + await transmuter + .connect(mockAlchemist) + .distribute(mockAlchemistAddress, stakeAmt); + await mineBlocks(ethers.provider, 10); + let userInfo = await transmuter.userInfo(await depositor.getAddress()); + let bufferInfo = await transmuter.bufferInfo(); + + expect(bufferInfo._buffer).equal('1000000000000000000000'); + expect(userInfo.pendingdivs).equal(stakeAmt.div(2)); + expect(userInfo.depositedAl).equal(stakeAmt); + expect(userInfo.inbucket).equal(0); + expect(userInfo.realised).equal(0); + }); + }); + }); +});