diff --git a/contracts/interfaces/wrapped-assets/IWrappedAsset.sol b/contracts/interfaces/wrapped-assets/IWrappedAsset.sol index 1e7b58f..25135f9 100644 --- a/contracts/interfaces/wrapped-assets/IWrappedAsset.sol +++ b/contracts/interfaces/wrapped-assets/IWrappedAsset.sol @@ -7,6 +7,11 @@ pragma solidity 0.7.6; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +/** + * @dev For upgradeable/clones contracts use inheritance from IWrappedAssetUpgradeable. + * @dev For dependencies in unit protocol use this interface + * @dev todo on update wsslp replace body with IWrappedAssetInternal + */ interface IWrappedAsset is IERC20 /* IERC20WithOptional */ { event Deposit(address indexed user, uint256 amount); diff --git a/contracts/interfaces/wrapped-assets/IWrappedAssetInternal.sol b/contracts/interfaces/wrapped-assets/IWrappedAssetInternal.sol new file mode 100644 index 0000000..8c74d35 --- /dev/null +++ b/contracts/interfaces/wrapped-assets/IWrappedAssetInternal.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: bsl-1.1 + +/* + Copyright 2022 Unit Protocol: Artem Zakharov (az@unit.xyz). +*/ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @dev wrapped assets methods/events. To inherit by IWrappedAsset* interfaces + */ +interface IWrappedAssetInternal { + + event Deposit(address indexed user, uint256 amount); + event Withdraw(address indexed user, uint256 amount); + event PositionMoved(address indexed userFrom, address indexed userTo, uint256 amount); + + event EmergencyWithdraw(address indexed user, uint256 amount); + event TokenWithdraw(address indexed user, address token, uint256 amount); + + event FeeChanged(uint256 newFeePercent); + event FeeReceiverChanged(address newFeeReceiver); + event AllowedBoneLockerSelectorAdded(address boneLocker, bytes4 selector); + event AllowedBoneLockerSelectorRemoved(address boneLocker, bytes4 selector); + + /** + * @notice Get underlying token + */ + function getUnderlyingToken() external view returns (IERC20); + + /** + * @notice deposit underlying token and send wrapped token to user + * @dev Important! Only user or trusted contracts must be able to call this method + */ + function deposit(address _userAddr, uint256 _amount) external; + + /** + * @notice get wrapped token and return underlying + * @dev Important! Only user or trusted contracts must be able to call this method + */ + function withdraw(address _userAddr, uint256 _amount) external; + + /** + * @notice get pending reward amount for user if reward is supported + */ + function pendingReward(address _userAddr) external view returns (uint256); + + /** + * @notice claim pending reward for user if reward is supported + */ + function claimReward(address _userAddr) external; + + /** + * @notice Manually move position (or its part) to another user (for example in case of liquidation) + * @dev Important! Only trusted contracts must be able to call this method + */ + function movePosition(address _userAddrFrom, address _userAddrTo, uint256 _amount) external; + + /** + * @dev function for checks that asset is unitprotocol wrapped asset. + * @dev For wrapped assets must return keccak256("UnitProtocolWrappedAsset") + */ + function isUnitProtocolWrappedAsset() external view returns (bytes32); +} diff --git a/contracts/interfaces/wrapped-assets/IWrappedAssetUpgradeable.sol b/contracts/interfaces/wrapped-assets/IWrappedAssetUpgradeable.sol new file mode 100644 index 0000000..ee5bed7 --- /dev/null +++ b/contracts/interfaces/wrapped-assets/IWrappedAssetUpgradeable.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: bsl-1.1 + +/* + Copyright 2021 Unit Protocol: Artem Zakharov (az@unit.xyz). +*/ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "./IWrappedAssetInternal.sol"; + +/** + * @dev for usage with upgradeable/clones contracts. Methods/events must be the same as in IWrappedAsset + * @dev For dependencies in unit protocol use IWrappedAsset + */ +interface IWrappedAssetUpgradeable is IERC20Upgradeable, IWrappedAssetInternal /* IERC20WithOptional */ { +} diff --git a/contracts/interfaces/wrapped-assets/sushi/IMasterChef.sol b/contracts/interfaces/wrapped-assets/sushi/IMasterChef.sol new file mode 100644 index 0000000..3528bb7 --- /dev/null +++ b/contracts/interfaces/wrapped-assets/sushi/IMasterChef.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: bsl-1.1 + +/* + Copyright 2021 Unit Protocol: Artem Zakharov (az@unit.xyz). +*/ +pragma solidity 0.7.6; + + +import "./ISushiToken.sol"; + +interface IMasterChef { + function sushi() external view returns (ISushiToken); + function poolInfo(uint256) external view returns (IERC20, uint256, uint256, uint256); + function poolLength() external view returns (uint256); + function userInfo(uint256, address) external view returns (uint256, uint256); + + function pendingSushi(uint256 _pid, address _user) external view returns (uint256); + function deposit(uint256 _pid, uint256 _amount) external; + function withdraw(uint256 _pid, uint256 _amount) external; + + function emergencyWithdraw(uint256 _pid) external; + +} \ No newline at end of file diff --git a/contracts/interfaces/wrapped-assets/sushi/ISushiToken.sol b/contracts/interfaces/wrapped-assets/sushi/ISushiToken.sol new file mode 100644 index 0000000..5ec326c --- /dev/null +++ b/contracts/interfaces/wrapped-assets/sushi/ISushiToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: bsl-1.1 + +/* + Copyright 2021 Unit Protocol: Artem Zakharov (az@unit.xyz). +*/ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface ISushiToken is IERC20 { + function mint(address _to, uint256 _amount) external; +} \ No newline at end of file diff --git a/contracts/interfaces/wrapped-assets/sushi/IWSLPFactory.sol b/contracts/interfaces/wrapped-assets/sushi/IWSLPFactory.sol new file mode 100644 index 0000000..44bf752 --- /dev/null +++ b/contracts/interfaces/wrapped-assets/sushi/IWSLPFactory.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: bsl-1.1 + +/* + Copyright 2022 Unit Protocol: Artem Zakharov (az@unit.xyz). +*/ +pragma solidity 0.7.6; + +interface IWSLPFactory { + struct FeeInfo { + address feeReceiver; + uint8 feePercent; + } + + event FeeChanged(address feeReceiver, uint8 feePercent); + event WrappedLpDeployed(address wrappedLp, uint rewardDistributorPoolId); + + function feeInfo() external view returns (address feeReceiver, uint8 feePercent); + function setFee(address _feeReceiver, uint8 _feePercent) external; + function deploy(uint256 _rewardDistributorPoolId) external returns (address wrappedLp); +} \ No newline at end of file diff --git a/contracts/wrapped-assets/sushi/WSLPFactory.sol b/contracts/wrapped-assets/sushi/WSLPFactory.sol new file mode 100644 index 0000000..f913ac8 --- /dev/null +++ b/contracts/wrapped-assets/sushi/WSLPFactory.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: bsl-1.1 + +/* + Copyright 2022 Unit Protocol: Artem Zakharov (az@unit.xyz). +*/ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/proxy/Clones.sol"; + +import "../../interfaces/wrapped-assets/sushi/IWSLPFactory.sol"; +import "../../interfaces/wrapped-assets/sushi/IMasterChef.sol"; +import "../../interfaces/IVault.sol"; +import "../../interfaces/IVaultParameters.sol"; +import "./WSLPUserProxy.sol"; +import "./WrappedSushiSwapLp.sol"; + + +/** + * @title WSLPFactory + **/ +contract WSLPFactory is IWSLPFactory, Auth2 { + + // these variables stored just for info + IMasterChef public immutable rewardDistributor; + IERC20 public immutable rewardToken; + + address public immutable wrappedSushiSwapLpImplementation; + address public immutable userProxyImplementation; + + FeeInfo public override feeInfo; + + mapping(uint => address) public wrappedLpByPoolId; + + constructor( + IVaultParameters _vaultParameters, + IMasterChef _rewardDistributor, + address _feeReceiver, + uint8 _feePercent + ) + Auth2(address(_vaultParameters)) + { + IERC20 rewardTokenInternal = _rewardDistributor.sushi(); + rewardToken = rewardTokenInternal; + + rewardDistributor = _rewardDistributor; + + feeInfo = FeeInfo(_feeReceiver, _feePercent); + + address userProxyImplementationInternal = address(new WSLPUserProxy(this, _rewardDistributor)); + userProxyImplementation = userProxyImplementationInternal; + + WrappedSushiSwapLp wrappedSushiSwapLpImplementationInternal = new WrappedSushiSwapLp( + _vaultParameters, _rewardDistributor, rewardTokenInternal, userProxyImplementationInternal + ); + wrappedSushiSwapLpImplementation = address(wrappedSushiSwapLpImplementationInternal); + + + // initialize implementations just not to allow to do it by somebody else + (IERC20 lpToken,,,) = _rewardDistributor.poolInfo(0); + WSLPUserProxy(userProxyImplementationInternal).initialize(0, lpToken); + + wrappedSushiSwapLpImplementationInternal.initialize(0); + + } + + function setFee(address _feeReceiver, uint8 _feePercent) public override onlyManager { + require(_feePercent <= 50, "Unit Protocol Wrapped Assets: INVALID_FEE"); + feeInfo = FeeInfo(_feeReceiver, _feePercent); + + emit FeeChanged(_feeReceiver, _feePercent); + } + + function deploy(uint256 _rewardDistributorPoolId) public override onlyManager returns (address wrappedLp) { + require(wrappedLpByPoolId[_rewardDistributorPoolId] == address(0), "Unit Protocol Wrapped Assets: ALREADY_DEPLOYED"); + + wrappedLp = Clones.clone(wrappedSushiSwapLpImplementation); + WrappedSushiSwapLp(wrappedLp).initialize(_rewardDistributorPoolId); + + wrappedLpByPoolId[_rewardDistributorPoolId] = wrappedLp; + emit WrappedLpDeployed(wrappedLp, _rewardDistributorPoolId); + } +} diff --git a/contracts/wrapped-assets/sushi/WSLPUserProxy.sol b/contracts/wrapped-assets/sushi/WSLPUserProxy.sol new file mode 100644 index 0000000..21dda48 --- /dev/null +++ b/contracts/wrapped-assets/sushi/WSLPUserProxy.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: bsl-1.1 + +/* + Copyright 2022 Unit Protocol: Artem Zakharov (az@unit.xyz). +*/ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/math/SafeMath.sol"; // have to use OZ safemath since it is used in WSLP + +import "../../interfaces/wrapped-assets/sushi/IMasterChef.sol"; +import "../../interfaces/wrapped-assets/sushi/IWSLPFactory.sol"; +import "../../helpers/TransferHelper.sol"; + +/** + * @title WSLPUserProxy + **/ +contract WSLPUserProxy { + using SafeMath for uint256; + + IWSLPFactory immutable factory; + + IMasterChef public immutable rewardDistributor; + IERC20 public immutable rewardToken; + + // to store in one slot rewardDistributorPoolId was reduced to uint96. 7*10^28 pools are quite enough + address public manager; + uint96 public rewardDistributorPoolId; + + modifier onlyManager() { + require(msg.sender == manager, "Unit Protocol Wrapped Assets: AUTH_FAILED"); + _; + } + + constructor(IWSLPFactory _factory, IMasterChef _rewardDistributor) { + factory = _factory; + rewardDistributor = _rewardDistributor; + + rewardToken = _rewardDistributor.sushi(); + } + + function initialize(uint96 _rewardDistributorPoolId, IERC20 _lpToken) public { + require(manager == address(0), "Unit Protocol Wrapped Assets: ALREADY_INITIALIZED"); + + manager = msg.sender; + rewardDistributorPoolId = _rewardDistributorPoolId; + + TransferHelper.safeApprove(address(_lpToken), address(rewardDistributor), type(uint256).max); + } + + /** + * @dev in case of change lp + */ + function approveLpToRewardDistributor(IERC20 _lpToken) public onlyManager { + TransferHelper.safeApprove(address(_lpToken), address(rewardDistributor), type(uint256).max); + } + + function deposit(uint256 _amount) public onlyManager { + rewardDistributor.deposit(rewardDistributorPoolId, _amount); + } + + function withdraw(IERC20 _lpToken, uint256 _amount, address _sentTokensTo) public onlyManager { + rewardDistributor.withdraw(rewardDistributorPoolId, _amount); + TransferHelper.safeTransfer(address(_lpToken), _sentTokensTo, _amount); + } + + function pendingReward() public view returns (uint) { + uint balance = rewardToken.balanceOf(address(this)); + uint pending = rewardDistributor.pendingSushi(rewardDistributorPoolId, address(this)); + + (uint amountWithoutFee,,) = _calcFee(balance.add(pending)); + return amountWithoutFee; + } + + function claimReward(address _user) public onlyManager { + rewardDistributor.deposit(rewardDistributorPoolId, 0); // get current reward (no separate methods) + + _sendAllRewardTokensToUser(_user); + } + + function _calcFee(uint _amount) internal view returns (uint amountWithoutFee, uint fee, address feeReceiver) { + (address _feeReceiver, uint8 _feePercent) = factory.feeInfo(); + if (_feePercent == 0 || _feeReceiver == address(0)) { + return (_amount, 0, address(0)); + } + + fee = _amount.mul(_feePercent).div(100); + return (_amount.sub(fee), fee, _feeReceiver); + } + + function _sendAllRewardTokensToUser(address _user) internal { + uint balance = rewardToken.balanceOf(address(this)); + + _sendRewardTokensToUser(_user, balance); + } + + function _sendRewardTokensToUser(address _user, uint _amount) internal { + (uint amountWithoutFee, uint fee, address feeReceiver) = _calcFee(_amount); + + if (fee > 0) { + TransferHelper.safeTransfer(address(rewardToken), feeReceiver, fee); + } + TransferHelper.safeTransfer(address(rewardToken), _user, amountWithoutFee); + } + + function emergencyWithdraw() public onlyManager { + rewardDistributor.emergencyWithdraw(rewardDistributorPoolId); + } + + function withdrawToken(address _token, address _user, uint _amount) public onlyManager { + if (_token == address(rewardToken)) { + _sendRewardTokensToUser(_user, _amount); + } else { + TransferHelper.safeTransfer(_token, _user, _amount); + } + } + + function getDepositedAmount() public view returns (uint amount) { + (amount, ) = rewardDistributor.userInfo(rewardDistributorPoolId, address (this)); + } +} diff --git a/contracts/wrapped-assets/sushi/WrappedSushiSwapLp.sol b/contracts/wrapped-assets/sushi/WrappedSushiSwapLp.sol new file mode 100644 index 0000000..8fdf274 --- /dev/null +++ b/contracts/wrapped-assets/sushi/WrappedSushiSwapLp.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: bsl-1.1 + +/* + Copyright 2021 Unit Protocol: Artem Zakharov (az@unit.xyz). +*/ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // 'contracts' are used intentionally, since there is no dependencies in this interface +import "@openzeppelin/contracts/proxy/Clones.sol"; // 'contracts' are used intentionally, since there is no dependencies in this library +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + +import "./WSLPUserProxy.sol"; +import "../../helpers/ReentrancyGuard.sol"; +import "../../helpers/TransferHelper.sol"; +import "../../Auth2.sol"; +import "../../interfaces/IVault.sol"; +import "../../interfaces/IERC20WithOptional.sol"; +import "../../interfaces/wrapped-assets/IWrappedAssetUpgradeable.sol"; +import "../../interfaces/wrapped-assets/sushi/IMasterChef.sol"; +import "../../interfaces/wrapped-assets/ISushiSwapLpToken.sol"; +import "../../interfaces/IVaultParameters.sol"; + +/** + * @title WrappedSushiSwapLp + **/ +contract WrappedSushiSwapLp is IWrappedAssetUpgradeable, Auth2, ERC20Upgradeable, ReentrancyGuard { + using SafeMathUpgradeable for uint256; + + bytes32 public constant override isUnitProtocolWrappedAsset = keccak256("UnitProtocolWrappedAsset"); + + IVault public immutable vault; + IMasterChef public immutable rewardDistributor; + IERC20 public immutable rewardToken; + + address public immutable userProxyImplementation; + + uint256 public rewardDistributorPoolId; + mapping(address => WSLPUserProxy) public usersProxies; + + constructor( + IVaultParameters _vaultParameters, + IMasterChef _rewardDistributor, + IERC20 _rewardToken, + address _userProxyImplementation + ) + Auth2(address(_vaultParameters)) + { + vault = IVault(_vaultParameters.vault()); + rewardDistributor = _rewardDistributor; + rewardToken = _rewardToken; + + userProxyImplementation = _userProxyImplementation; + } + + function initialize(uint256 _rewardDistributorPoolId) initializer public { + require(rewardDistributorPoolId < type(uint96).max, "Unit Protocol Wrapped Assets: TOO_MANY_POOLS"); // in user proxies pool id is stores in uint96 + rewardDistributorPoolId = _rewardDistributorPoolId; + + (IERC20 lpToken,,,) = rewardDistributor.poolInfo(_rewardDistributorPoolId); + address lpTokenAddr = address(lpToken); + string memory lpToken0Symbol = IERC20WithOptional(address(ISushiSwapLpToken(lpTokenAddr).token0())).symbol(); + string memory lpToken1Symbol = IERC20WithOptional(address(ISushiSwapLpToken(lpTokenAddr).token1())).symbol(); + + __ERC20_init( + string( + abi.encodePacked( + "Wrapped by Unit ", + IERC20WithOptional(lpTokenAddr).name(), + " ", + lpToken0Symbol, + "-", + lpToken1Symbol + ) + ), + string( + abi.encodePacked( + "wu", + IERC20WithOptional(lpTokenAddr).symbol(), + lpToken0Symbol, + lpToken1Symbol + ) + ) + ); + + _setupDecimals(IERC20WithOptional(lpTokenAddr).decimals()); + } + + /** + * @notice Approve lp token to spend from user proxy (in case of change lp) + */ + function approveLpToRewardDistributor() public nonReentrant { + WSLPUserProxy userProxy = _requireUserProxy(msg.sender); + IERC20 lpToken = getUnderlyingToken(); + + userProxy.approveLpToRewardDistributor(lpToken); + } + + /** + * @notice Get tokens from user, send them to the reward distributor, send to user wrapped tokens + * @dev only user or CDPManager could call this method + */ + function deposit(address _user, uint256 _amount) public override nonReentrant { + require(_amount > 0, "Unit Protocol Wrapped Assets: INVALID_AMOUNT"); + require(msg.sender == _user || vaultParameters.canModifyVault(msg.sender), "Unit Protocol Wrapped Assets: AUTH_FAILED"); + + IERC20 lpToken = getUnderlyingToken(); + WSLPUserProxy userProxy = _getOrCreateUserProxy(_user, lpToken); + + // get tokens from user, need approve of lp tokens to pool + TransferHelper.safeTransferFrom(address(lpToken), _user, address(userProxy), _amount); + + // deposit them to the reward distributor + userProxy.deposit(_amount); + + // wrapped tokens to user + _mint(_user, _amount); + + emit Deposit(_user, _amount); + } + + /** + * @notice Unwrap tokens, withdraw from the reward distributor and send them to user + * @dev only user or CDPManager could call this method + */ + function withdraw(address _user, uint256 _amount) public override nonReentrant { + require(_amount > 0, "Unit Protocol Wrapped Assets: INVALID_AMOUNT"); + require(msg.sender == _user || vaultParameters.canModifyVault(msg.sender), "Unit Protocol Wrapped Assets: AUTH_FAILED"); + + IERC20 lpToken = getUnderlyingToken(); + WSLPUserProxy userProxy = _requireUserProxy(_user); + + // get wrapped tokens from user + _burn(_user, _amount); + + // withdraw funds from the reward distributor + userProxy.withdraw(lpToken, _amount, _user); + + emit Withdraw(_user, _amount); + } + + /** + * @notice Manually move position (or its part) to another user (for example in case of liquidation) + * @dev Important! Use only with additional token transferring outside this function (example: liquidation - tokens are in vault and transferred by vault) + * @dev only CDPManager could call this method + */ + function movePosition(address _userFrom, address _userTo, uint256 _amount) public override nonReentrant hasVaultAccess { + require(_userFrom != address(vault) && _userTo != address(vault), "Unit Protocol Wrapped Assets: NOT_ALLOWED_FOR_VAULT"); + if (_userFrom == _userTo || _amount == 0) { + return; + } + + IERC20 lpToken = getUnderlyingToken(); + WSLPUserProxy userFromProxy = _requireUserProxy(_userFrom); + WSLPUserProxy userToProxy = _getOrCreateUserProxy(_userTo, lpToken); + + userFromProxy.withdraw(lpToken, _amount, address(userToProxy)); + userToProxy.deposit(_amount); + + emit Withdraw(_userFrom, _amount); + emit Deposit(_userTo, _amount); + emit PositionMoved(_userFrom, _userTo, _amount); + } + + /** + * @notice Calculates pending reward for user. + */ + function pendingReward(address _user) public override view returns (uint256) { + WSLPUserProxy userProxy = usersProxies[_user]; + if (address(userProxy) == address(0)) { + return 0; + } + + return userProxy.pendingReward(); + } + + /** + * @notice Claim pending direct reward for user. + */ + function claimReward(address _user) public override nonReentrant { + require(_user == msg.sender, "Unit Protocol Wrapped Assets: AUTH_FAILED"); + + WSLPUserProxy userProxy = _requireUserProxy(_user); + userProxy.claimReward(_user); + } + + /** + * @notice get LP token + * @dev not immutable since it could be changed in the reward distributor + */ + function getUnderlyingToken() public override view returns (IERC20) { + (IERC20 _lpToken,,,) = rewardDistributor.poolInfo(rewardDistributorPoolId); + + return _lpToken; + } + + /** + * @notice Withdraw tokens from the reward distributor to user proxy without caring about rewards. EMERGENCY ONLY. + * @notice To withdraw tokens from user proxy to user use `withdrawToken` + */ + function emergencyWithdraw() public nonReentrant { + WSLPUserProxy userProxy = _requireUserProxy(msg.sender); + + uint amount = userProxy.getDepositedAmount(); + _burn(msg.sender, amount); + assert(balanceOf(msg.sender) == 0); + + userProxy.emergencyWithdraw(); + + emit EmergencyWithdraw(msg.sender, amount); + } + + function withdrawToken(address _token, uint _amount) public nonReentrant { + WSLPUserProxy userProxy = _requireUserProxy(msg.sender); + userProxy.withdrawToken(_token, msg.sender, _amount); + + emit TokenWithdraw(msg.sender, _token, _amount); + } + + /** + * @dev No direct transfers between users allowed since we store positions info in userInfo. + */ + function _transfer(address sender, address recipient, uint256 amount) internal override { + require(msg.sender == address(vault), "Unit Protocol: AUTH_FAILED"); // do not use onlyVault to save some gas by avoiding external call + require(sender == address(vault) || recipient == address(vault), "Unit Protocol Wrapped Assets: AUTH_FAILED"); + super._transfer(sender, recipient, amount); + } + + function _requireUserProxy(address _user) internal view returns (WSLPUserProxy userProxy) { + userProxy = usersProxies[_user]; + require(address(userProxy) != address(0), "Unit Protocol Wrapped Assets: NO_DEPOSIT"); + } + + function _getOrCreateUserProxy(address _user, IERC20 _lpToken) internal returns (WSLPUserProxy userProxy) { + userProxy = usersProxies[_user]; + if (address(userProxy) == address(0)) { + // create new + userProxy = WSLPUserProxy(Clones.clone(userProxyImplementation)); + userProxy.initialize(uint96(rewardDistributorPoolId), _lpToken); // overflow is checked in initialize + + usersProxies[_user] = userProxy; + } + } +} diff --git a/contracts/wrapped-assets/sushi/test-helpers/MasterChef_Mock.sol b/contracts/wrapped-assets/sushi/test-helpers/MasterChef_Mock.sol new file mode 100644 index 0000000..b3a5c3a --- /dev/null +++ b/contracts/wrapped-assets/sushi/test-helpers/MasterChef_Mock.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: unknown +// Origin Sushi contracts slightly changed for run in tests +// Origin contract addr https://etherscan.io/address/0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd#code + +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../../../interfaces/wrapped-assets/sushi/ISushiToken.sol"; +import "../../../interfaces/wrapped-assets/sushi/IMasterChef.sol"; + +interface IMigratorChef { + // Perform LP token migration from legacy UniswapV2 to SushiSwap. + // Take the current LP token address and return the new LP token address. + // Migrator should have full access to the caller's LP token. + // Return the new LP token address. + // + // XXX Migrator must have allowance access to UniswapV2 LP tokens. + // SushiSwap must mint EXACTLY the same amount of SushiSwap LP tokens or + // else something bad will happen. Traditional UniswapV2 does not + // do that so be careful! + function migrate(IERC20 token) external returns (IERC20); +} + +// MasterChef is the master of Sushi. He can make Sushi and he is a fair guy. +// +// Note that it's ownable and the owner wields tremendous power. The ownership +// will be transferred to a governance smart contract once SUSHI is sufficiently +// distributed and the community can show to govern itself. +// +// Have fun reading it. Hopefully it's bug-free. God bless. +contract MasterChef_Mock is IMasterChef, Ownable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + // Info of each user. + struct UserInfo { + uint256 amount; // How many LP tokens the user has provided. + uint256 rewardDebt; // Reward debt. See explanation below. + // + // We do some fancy math here. Basically, any point in time, the amount of SUSHIs + // entitled to a user but is pending to be distributed is: + // + // pending reward = (user.amount * pool.accSushiPerShare) - user.rewardDebt + // + // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens: + // 1. The pool's `accSushiPerShare` (and `lastRewardBlock`) gets updated. + // 2. User receives the pending reward sent to his/her address. + // 3. User's `amount` gets updated. + // 4. User's `rewardDebt` gets updated. + } + + // Info of each pool. + struct PoolInfo { + IERC20 lpToken; // Address of LP token contract. + uint256 allocPoint; // How many allocation points assigned to this pool. SUSHIs to distribute per block. + uint256 lastRewardBlock; // Last block number that SUSHIs distribution occurs. + uint256 accSushiPerShare; // Accumulated SUSHIs per share, times 1e12. See below. + } + + // The SUSHI TOKEN! + ISushiToken public override sushi; + // Dev address. + address public devaddr; + // Block number when bonus SUSHI period ends. + uint256 public bonusEndBlock; + // SUSHI tokens created per block. + uint256 public sushiPerBlock; + // Bonus muliplier for early sushi makers. + uint256 public constant BONUS_MULTIPLIER = 10; + // The migrator contract. It has a lot of power. Can only be set through governance (owner). + IMigratorChef public migrator; + + // Info of each pool. + PoolInfo[] public override poolInfo; + // Info of each user that stakes LP tokens. + mapping (uint256 => mapping (address => UserInfo)) public override userInfo; + // Total allocation poitns. Must be the sum of all allocation points in all pools. + uint256 public totalAllocPoint = 0; + // The block number when SUSHI mining starts. + uint256 public startBlock; + + event Deposit(address indexed user, uint256 indexed pid, uint256 amount); + event Withdraw(address indexed user, uint256 indexed pid, uint256 amount); + event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount); + + constructor( + ISushiToken _sushi, + address _devaddr, + uint256 _sushiPerBlock, + uint256 _startBlock, + uint256 _bonusEndBlock + ) { + sushi = _sushi; + devaddr = _devaddr; + sushiPerBlock = _sushiPerBlock; + bonusEndBlock = _bonusEndBlock; + startBlock = _startBlock; + } + + function poolLength() external view override returns (uint256) { + return poolInfo.length; + } + + // Add a new lp to the pool. Can only be called by the owner. + // XXX DO NOT add the same LP token more than once. Rewards will be messed up if you do. + function add(uint256 _allocPoint, IERC20 _lpToken, bool _withUpdate) public onlyOwner { + if (_withUpdate) { + massUpdatePools(); + } + uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; + totalAllocPoint = totalAllocPoint.add(_allocPoint); + poolInfo.push(PoolInfo({ + lpToken: _lpToken, + allocPoint: _allocPoint, + lastRewardBlock: lastRewardBlock, + accSushiPerShare: 0 + })); + } + + // Update the given pool's SUSHI allocation point. Can only be called by the owner. + function set(uint256 _pid, uint256 _allocPoint, bool _withUpdate) public onlyOwner { + if (_withUpdate) { + massUpdatePools(); + } + totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add(_allocPoint); + poolInfo[_pid].allocPoint = _allocPoint; + } + + // Set the migrator contract. Can only be called by the owner. + function setMigrator(IMigratorChef _migrator) public onlyOwner { + migrator = _migrator; + } + + // Migrate lp token to another lp contract. Can be called by anyone. We trust that migrator contract is good. + function migrate(uint256 _pid) public { + require(address(migrator) != address(0), "migrate: no migrator"); + PoolInfo storage pool = poolInfo[_pid]; + IERC20 lpToken = pool.lpToken; + uint256 bal = lpToken.balanceOf(address(this)); + lpToken.safeApprove(address(migrator), bal); + IERC20 newLpToken = migrator.migrate(lpToken); + require(bal == newLpToken.balanceOf(address(this)), "migrate: bad"); + pool.lpToken = newLpToken; + } + + // Return reward multiplier over the given _from to _to block. + function getMultiplier(uint256 _from, uint256 _to) public view returns (uint256) { + if (_to <= bonusEndBlock) { + return _to.sub(_from).mul(BONUS_MULTIPLIER); + } else if (_from >= bonusEndBlock) { + return _to.sub(_from); + } else { + return bonusEndBlock.sub(_from).mul(BONUS_MULTIPLIER).add( + _to.sub(bonusEndBlock) + ); + } + } + + // View function to see pending SUSHIs on frontend. + function pendingSushi(uint256 _pid, address _user) external view override returns (uint256) { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][_user]; + uint256 accSushiPerShare = pool.accSushiPerShare; + uint256 lpSupply = pool.lpToken.balanceOf(address(this)); + if (block.number > pool.lastRewardBlock && lpSupply != 0) { + uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number); + uint256 sushiReward = multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div(totalAllocPoint); + accSushiPerShare = accSushiPerShare.add(sushiReward.mul(1e12).div(lpSupply)); + } + return user.amount.mul(accSushiPerShare).div(1e12).sub(user.rewardDebt); + } + + // Update reward vairables for all pools. Be careful of gas spending! + function massUpdatePools() public { + uint256 length = poolInfo.length; + for (uint256 pid = 0; pid < length; ++pid) { + updatePool(pid); + } + } + + // Update reward variables of the given pool to be up-to-date. + function updatePool(uint256 _pid) public { + PoolInfo storage pool = poolInfo[_pid]; + if (block.number <= pool.lastRewardBlock) { + return; + } + uint256 lpSupply = pool.lpToken.balanceOf(address(this)); + if (lpSupply == 0) { + pool.lastRewardBlock = block.number; + return; + } + uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number); + uint256 sushiReward = multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div(totalAllocPoint); + sushi.mint(devaddr, sushiReward.div(10)); + sushi.mint(address(this), sushiReward); + pool.accSushiPerShare = pool.accSushiPerShare.add(sushiReward.mul(1e12).div(lpSupply)); + pool.lastRewardBlock = block.number; + } + + // Deposit LP tokens to MasterChef for SUSHI allocation. + function deposit(uint256 _pid, uint256 _amount) public override { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][msg.sender]; + updatePool(_pid); + if (user.amount > 0) { + uint256 pending = user.amount.mul(pool.accSushiPerShare).div(1e12).sub(user.rewardDebt); + safeSushiTransfer(msg.sender, pending); + } + pool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount); + user.amount = user.amount.add(_amount); + user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e12); + emit Deposit(msg.sender, _pid, _amount); + } + + // Withdraw LP tokens from MasterChef. + function withdraw(uint256 _pid, uint256 _amount) public override { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][msg.sender]; + require(user.amount >= _amount, "withdraw: not good"); + updatePool(_pid); + uint256 pending = user.amount.mul(pool.accSushiPerShare).div(1e12).sub(user.rewardDebt); + safeSushiTransfer(msg.sender, pending); + user.amount = user.amount.sub(_amount); + user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e12); + pool.lpToken.safeTransfer(address(msg.sender), _amount); + emit Withdraw(msg.sender, _pid, _amount); + } + + // Withdraw without caring about rewards. EMERGENCY ONLY. + function emergencyWithdraw(uint256 _pid) public override { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][msg.sender]; + pool.lpToken.safeTransfer(address(msg.sender), user.amount); + emit EmergencyWithdraw(msg.sender, _pid, user.amount); + user.amount = 0; + user.rewardDebt = 0; + } + + // Safe sushi transfer function, just in case if rounding error causes pool to not have enough SUSHIs. + function safeSushiTransfer(address _to, uint256 _amount) internal { + uint256 sushiBal = sushi.balanceOf(address(this)); + if (_amount > sushiBal) { + sushi.transfer(_to, sushiBal); + } else { + sushi.transfer(_to, _amount); + } + } + + // Update dev address by the previous dev. + function dev(address _devaddr) public { + require(msg.sender == devaddr, "dev: wut?"); + devaddr = _devaddr; + } +} \ No newline at end of file diff --git a/contracts/wrapped-assets/sushi/test-helpers/MigratorChef_Mock.sol b/contracts/wrapped-assets/sushi/test-helpers/MigratorChef_Mock.sol new file mode 100644 index 0000000..285c7a9 --- /dev/null +++ b/contracts/wrapped-assets/sushi/test-helpers/MigratorChef_Mock.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: bsl-1.1 + +/* + Copyright 2021 Unit Protocol: Artem Zakharov (az@unit.xyz). +*/ +pragma solidity 0.7.6; + +import "./MasterChef_Mock.sol"; + +contract MigratorChef_Mock is IMigratorChef { + + IERC20 public newToken; + + function migrate(IERC20 token) external override returns (IERC20) { + newToken.transfer(msg.sender, token.balanceOf(msg.sender)); + return newToken; + } + + function setNewToken(IERC20 token) public { + newToken = token; + } +} \ No newline at end of file diff --git a/contracts/wrapped-assets/sushi/test-helpers/SushiToken_Mock.sol b/contracts/wrapped-assets/sushi/test-helpers/SushiToken_Mock.sol new file mode 100644 index 0000000..ad2a6cf --- /dev/null +++ b/contracts/wrapped-assets/sushi/test-helpers/SushiToken_Mock.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: unknown +// Origin Sushi contracts slightly changed for run in tests +// Origin contract addr https://etherscan.io/address/0x6b3595068778dd592e39a122f4f5a5cf09c90fe2#code + +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +// SushiToken with Governance. +contract SushiToken_Mock is ERC20("SushiToken", "SUSHI"), Ownable { + using SafeMath for uint256; + + /// @notice Creates `_amount` token to `_to`. Must only be called by the owner (MasterChef). + function mint(address _to, uint256 _amount) public onlyOwner { + _mint(_to, _amount); + _moveDelegates(address(0), _delegates[_to], _amount); + } + + // Copied and modified from YAM code: + // https://github.com/yam-finance/yam-protocol/blob/master/contracts/token/YAMGovernanceStorage.sol + // https://github.com/yam-finance/yam-protocol/blob/master/contracts/token/YAMGovernance.sol + // Which is copied and modified from COMPOUND: + // https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/Comp.sol + + /// @dev A record of each accounts delegate + mapping (address => address) internal _delegates; + + /// @notice A checkpoint for marking number of votes from a given block + struct Checkpoint { + uint32 fromBlock; + uint256 votes; + } + + /// @notice A record of votes checkpoints for each account, by index + mapping (address => mapping (uint32 => Checkpoint)) public checkpoints; + + /// @notice The number of checkpoints for each account + mapping (address => uint32) public numCheckpoints; + + /// @notice The EIP-712 typehash for the contract's domain + bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + + /// @notice The EIP-712 typehash for the delegation struct used by the contract + bytes32 public constant DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + /// @notice A record of states for signing / validating signatures + mapping (address => uint) public nonces; + + /// @notice An event thats emitted when an account changes its delegate + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /// @notice An event thats emitted when a delegate account's vote balance changes + event DelegateVotesChanged(address indexed delegate, uint previousBalance, uint newBalance); + + /** + * @notice Delegate votes from `msg.sender` to `delegatee` + * @param delegator The address to get delegatee for + */ + function delegates(address delegator) + external + view + returns (address) + { + return _delegates[delegator]; + } + + /** + * @notice Delegate votes from `msg.sender` to `delegatee` + * @param delegatee The address to delegate votes to + */ + function delegate(address delegatee) external { + return _delegate(msg.sender, delegatee); + } + + /** + * @notice Delegates votes from signatory to `delegatee` + * @param delegatee The address to delegate votes to + * @param nonce The contract state required to match the signature + * @param expiry The time at which to expire the signature + * @param v The recovery byte of the signature + * @param r Half of the ECDSA signature pair + * @param s Half of the ECDSA signature pair + */ + function delegateBySig( + address delegatee, + uint nonce, + uint expiry, + uint8 v, + bytes32 r, + bytes32 s + ) + external + { + bytes32 domainSeparator = keccak256( + abi.encode( + DOMAIN_TYPEHASH, + keccak256(bytes(name())), + getChainId(), + address(this) + ) + ); + + bytes32 structHash = keccak256( + abi.encode( + DELEGATION_TYPEHASH, + delegatee, + nonce, + expiry + ) + ); + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + domainSeparator, + structHash + ) + ); + + address signatory = ecrecover(digest, v, r, s); + require(signatory != address(0), "SUSHI::delegateBySig: invalid signature"); + require(nonce == nonces[signatory]++, "SUSHI::delegateBySig: invalid nonce"); + require(block.timestamp <= expiry, "SUSHI::delegateBySig: signature expired"); + return _delegate(signatory, delegatee); + } + + /** + * @notice Gets the current votes balance for `account` + * @param account The address to get votes balance + * @return The number of current votes for `account` + */ + function getCurrentVotes(address account) + external + view + returns (uint256) + { + uint32 nCheckpoints = numCheckpoints[account]; + return nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0; + } + + /** + * @notice Determine the prior number of votes for an account as of a block number + * @dev Block number must be a finalized block or else this function will revert to prevent misinformation. + * @param account The address of the account to check + * @param blockNumber The block number to get the vote balance at + * @return The number of votes the account had as of the given block + */ + function getPriorVotes(address account, uint blockNumber) + external + view + returns (uint256) + { + require(blockNumber < block.number, "SUSHI::getPriorVotes: not yet determined"); + + uint32 nCheckpoints = numCheckpoints[account]; + if (nCheckpoints == 0) { + return 0; + } + + // First check most recent balance + if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) { + return checkpoints[account][nCheckpoints - 1].votes; + } + + // Next check implicit zero balance + if (checkpoints[account][0].fromBlock > blockNumber) { + return 0; + } + + uint32 lower = 0; + uint32 upper = nCheckpoints - 1; + while (upper > lower) { + uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow + Checkpoint memory cp = checkpoints[account][center]; + if (cp.fromBlock == blockNumber) { + return cp.votes; + } else if (cp.fromBlock < blockNumber) { + lower = center; + } else { + upper = center - 1; + } + } + return checkpoints[account][lower].votes; + } + + function _delegate(address delegator, address delegatee) + internal + { + address currentDelegate = _delegates[delegator]; + uint256 delegatorBalance = balanceOf(delegator); // balance of underlying SUSHIs (not scaled); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveDelegates(currentDelegate, delegatee, delegatorBalance); + } + + function _moveDelegates(address srcRep, address dstRep, uint256 amount) internal { + if (srcRep != dstRep && amount > 0) { + if (srcRep != address(0)) { + // decrease old representative + uint32 srcRepNum = numCheckpoints[srcRep]; + uint256 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0; + uint256 srcRepNew = srcRepOld.sub(amount); + _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew); + } + + if (dstRep != address(0)) { + // increase new representative + uint32 dstRepNum = numCheckpoints[dstRep]; + uint256 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0; + uint256 dstRepNew = dstRepOld.add(amount); + _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew); + } + } + } + + function _writeCheckpoint( + address delegatee, + uint32 nCheckpoints, + uint256 oldVotes, + uint256 newVotes + ) + internal + { + uint32 blockNumber = safe32(block.number, "SUSHI::_writeCheckpoint: block number exceeds 32 bits"); + + if (nCheckpoints > 0 && checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber) { + checkpoints[delegatee][nCheckpoints - 1].votes = newVotes; + } else { + checkpoints[delegatee][nCheckpoints] = Checkpoint(blockNumber, newVotes); + numCheckpoints[delegatee] = nCheckpoints + 1; + } + + emit DelegateVotesChanged(delegatee, oldVotes, newVotes); + } + + function safe32(uint n, string memory errorMessage) internal pure returns (uint32) { + require(n < 2**32, errorMessage); + return uint32(n); + } + + function getChainId() internal pure returns (uint) { + uint256 chainId; + assembly { chainId := chainid() } + return chainId; + } +} \ No newline at end of file diff --git a/hardhat.config.js b/hardhat.config.js index f35cfc3..c0e2e42 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -2,6 +2,7 @@ require("dotenv").config({ path: require("find-config")(".env") }); const { types } = require("hardhat/config") const {createDeployment: createCoreDeployment} = require("./lib/deployments/core"); const {createDeployment: createWrappedSSLPDeployment} = require("./lib/deployments/wrappedSSLP"); +const {createDeployment: createWrappedSLPDeployment} = require("./lib/deployments/wrappedSLP"); const {createDeployment: createSwappersDeployment} = require("./lib/deployments/swappers"); const {runDeployment} = require("./test/helpers/deployUtils"); const {VAULT_PARAMETERS} = require("./network_constants"); @@ -65,6 +66,30 @@ task('deployWrappedSslp', 'Deploy wrapped sslp') console.log('Success!', deployed); }); +task('deployWrappedSlp', 'Deploy wrapped sslp') + .addParam('manager', 'Address of a manager account/contract') + .addParam('feeReceiver', 'Address of fee receiver') + .addParam('feePercent', 'Fee percent', 50, types.int) + .addOptionalParam('rewardDistributor', 'Address of MasterChief contract', '0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd', types.string) + .addOptionalParam('noVerify', 'Skip contracts verification on *scan block explorer', false, types.boolean) + .setAction(async (taskArgs) => { + await hre.run("compile"); + + const deployer = taskArgs.deployer ? taskArgs.deployer : (await ethers.getSigners())[0].address; + const deployment = await createWrappedSLPDeployment({ + deployer, + manager: taskArgs.manager, + vaultParameters: VAULT_PARAMETERS, + rewardDistributor: taskArgs.rewardDistributor, + feeReceiver: taskArgs.feeReceiver, + feePercent: taskArgs.feePercent, + }); + + const deployed = await runDeployment(deployment, {deployer, verify: !taskArgs.noVerify}); + + console.log('Success!', deployed); + }); + task('deploySwappers', 'Deploy swappers') .addOptionalParam('noVerify', 'Skip contracts verification on *scan block explorer', false, types.boolean) .setAction(async (taskArgs) => { diff --git a/lib/deployments/wrappedSLP.js b/lib/deployments/wrappedSLP.js new file mode 100644 index 0000000..ec3d9ad --- /dev/null +++ b/lib/deployments/wrappedSLP.js @@ -0,0 +1,15 @@ +// Sushi +const createDeployment = async function(args) { + const {deployer, manager, vaultParameters, rewardDistributor, feeReceiver, feePercent} = args; + + const script = [ + ['WSLPFactory', vaultParameters, rewardDistributor, feeReceiver, feePercent], + ]; + + return script; +}; + + +module.exports = { + createDeployment, +}; diff --git a/network_constants.js b/network_constants.js index 5e93f00..3bc6bf6 100644 --- a/network_constants.js +++ b/network_constants.js @@ -4,6 +4,11 @@ module.exports = { USDP: "0x1456688345527bE1f37E9e627DA0837D6f08C925", + MULTISIG_CONTROL: '0xae37E8f9a3f960eE090706Fa4db41Ca2f2C56Cb8', + MULTISIG_FEE: '0xB3E75687652D33D6F5CaD5B113619641E4F6535B', + TEST_WALLET: '0x8442e4fcbba519b4f4c1ea1fce57a5379c55906c', + + VAULT: '0xb1cFF81b9305166ff1EFc49A129ad2AfCd7BCf19', VAULT_PARAMETERS: "0xB46F8CF42e504Efe8BEf895f848741daA55e9f1D", VAULT_MANAGER_PARAMETERS: '0x203153522B9EAef4aE17c6e99851EE7b2F7D312E', @@ -16,6 +21,7 @@ module.exports = { ORACLE_REGISTRY: '0x75fBFe26B21fd3EA008af0C764949f8214150C8f', ORACLE_CHAINLINK: "0x54b21C140F5463e1fDa69B934da619eAaa61f1CA", // ChainlinkedOracleMainAsset ORACLE_POOL_TOKEN: "0xd88e1F40b6CD9793aa10A6C3ceEA1d01C2a507f9", // OraclePoolToken + ORACLE_WRAPPED_TO_UNDERLYING: '0x220Ea780a484c18fd0Ab252014c58299759a1Fbd', // WrappedToUnderlyingOracle CURVE_USDP_3CRV_POOL: "0x42d7025938bEc20B69cBae5A77421082407f053A", diff --git a/package.json b/package.json index 5a5da7f..dbc9216 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@nomiclabs/hardhat-waffle": "^2.0.1", "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/contracts": "3.4.0", + "@openzeppelin/contracts-upgradeable": "^3.4.0", "@truffle/hdwallet-provider": "^1.5.1", "chai": "^4.2.0", "chai-arrays": "^2.2.0", diff --git a/scripts/deployTestWrappedSlp.js b/scripts/deployTestWrappedSlp.js new file mode 100644 index 0000000..6c20ddb --- /dev/null +++ b/scripts/deployTestWrappedSlp.js @@ -0,0 +1,212 @@ +// temp script for testing wrapped slp +// +// create key in alchemyapi.io +// run `npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/` +// run `npx hardhat --network localhost run scripts/deployTestWrappedSslp.js` +// connect with dapp + +const {deployContract, attachContract, weiToEther, ether} = require("../test/helpers/ethersUtils"); +const {ORACLE_TYPE_WRAPPED_TO_UNDERLYING_KEYDONIX, ORACLE_TYPE_WRAPPED_TO_UNDERLYING, + ORACLE_TYPE_UNISWAP_V2_POOL_TOKEN, + PARAM_FORCE_TRANSFER_ASSET_TO_OWNER_ON_LIQUIDATION, PARAM_FORCE_MOVE_WRAPPED_ASSET_POSITION_ON_LIQUIDATION +} = require("../lib/constants"); +const {createDeployment: createSwappersDeployment} = require("../lib/deployments/swappers"); +const {runDeployment} = require("../test/helpers/deployUtils"); +const {VAULT, VAULT_PARAMETERS, VAULT_MANAGER_PARAMETERS, ORACLE_REGISTRY, VAULT_MANAGER_BORROW_FEE_PARAMETERS, + CDP_REGISTRY, USDP, WETH, ORACLE_WRAPPED_TO_UNDERLYING, MULTISIG_CONTROL, TEST_WALLET, MULTISIG_FEE +} = require("../network_constants"); +const {ethers} = require("hardhat"); + + +const REWARD_DISTRIBUTOR = '0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd' +const REWARD_TOKEN = '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2' +const USDT_SLP = '0x06da0fd433C1A5d7a4faa01111c044910A184553' + +async function deploy() { + const [deployer, ] = await ethers.getSigners(); + + const usdp = await attachContract('USDP', USDP); + const weth = await attachContract('WETHMock', WETH); + const vault = await attachContract('IVault', VAULT); + const slpUsdt = await attachContract('IERC20', USDT_SLP); + + await ethers.provider.send("hardhat_impersonateAccount", [TEST_WALLET]); + const testWallet = await ethers.getSigner(TEST_WALLET) + await ethers.provider.send("hardhat_setBalance", [TEST_WALLET, '0x3635c9adc5dea00000' /* 1000Ether */]); + + await ethers.provider.send("hardhat_impersonateAccount", [MULTISIG_CONTROL]); + const multisig = await ethers.getSigner(MULTISIG_CONTROL) + await ethers.provider.send("hardhat_setBalance", [MULTISIG_CONTROL, '0x3635c9adc5dea00000' /* 1000Ether */]); + + const vaultParameters = await attachContract('VaultParameters', VAULT_PARAMETERS); + + const swappersRegistry = await deployContract("SwappersRegistry", VAULT_PARAMETERS); + + //////// cdp manager //////////////////////////////////////////// + // no keydonix since hardhat doesn't support proofs command + const cdpManager = await deployContract("CDPManager01", VAULT_MANAGER_PARAMETERS, VAULT_MANAGER_BORROW_FEE_PARAMETERS, ORACLE_REGISTRY, CDP_REGISTRY, swappersRegistry.address); + await vaultParameters.connect(multisig).setVaultAccess(cdpManager.address, true); + + console.log("cdpManager: " + cdpManager.address) + //////// end of cdp manager //////////////////////////////////////////// + + + //////// wrapped assets //////////////////////////////////////////// + const wrappedSlpUsdt = await deployContract('WrappedSushiSwapLp', VAULT_PARAMETERS, REWARD_DISTRIBUTOR, 0, MULTISIG_FEE); + + console.log("wrappedSlpUsdt: " + wrappedSlpUsdt.address) + console.log("sslp usdt: " + USDT_SLP) + //////// end of wrapped assets //////////////////////////////////////////// + + + //////// oracles //////////////////////////////////////////// + const oracleRegistry = await attachContract('OracleRegistry', ORACLE_REGISTRY); + const wrappedOracle = await attachContract('WrappedToUnderlyingOracle', ORACLE_WRAPPED_TO_UNDERLYING); + + await wrappedOracle.connect(multisig).setUnderlying(wrappedSlpUsdt.address, USDT_SLP); + + await oracleRegistry.connect(multisig).setOracleTypeForAsset(wrappedSlpUsdt.address, ORACLE_TYPE_WRAPPED_TO_UNDERLYING) + + await oracleRegistry.connect(multisig).setOracleTypeForAsset(USDT_SLP, ORACLE_TYPE_UNISWAP_V2_POOL_TOKEN) + + console.log('oracles check') + const price = BigInt((await wrappedOracle.assetToUsd(wrappedSlpUsdt.address, '1000000000000000000')).toString()) / (BigInt(2)**BigInt(112)) / (BigInt(10)**BigInt(18)); + console.log('wrapped usdt lp price', price) + const poolOracle = await attachContract('OraclePoolToken', '0xd88e1F40b6CD9793aa10A6C3ceEA1d01C2a507f9') + const price2 = BigInt((await poolOracle.assetToUsd(USDT_SLP, '1000000000000000000')).toString()) / (BigInt(2)**BigInt(112)) / (BigInt(10)**BigInt(18)); + console.log('usdt lp price', price2) + //////// end of oracles //////////////////////////////////////////// + + + //////// collaterals //////////////////////////////////////////// + const vaultManagerParameters = await attachContract('VaultManagerParameters', VAULT_MANAGER_PARAMETERS); + + await vaultManagerParameters.connect(multisig).setCollateral(//tx + wrappedSlpUsdt.address, + '900', // stability fee + '5', // liquidation fee + '49', // initial collateralization + '50', // liquidation ratio + '0', // liquidation discount (3 decimals) + '100', // devaluation period in blocks + '1000000000000000000000', // debt limit + [ORACLE_TYPE_WRAPPED_TO_UNDERLYING], // enabled oracles + 0, + 0, + ); + + // WARNING not for prod!! for testing of liquidations + await vaultManagerParameters.connect(multisig).setInitialCollateralRatio(wrappedSlpUsdt.address, 100); + //////// end of collaterals //////////////////////////////////////////// + + + //////// auction //////////////////////////////////////////// + const parameters = await attachContract( + 'AssetsBooleanParameters', + "0xcc33c2840b65c0a4ac4015c650dd20dc3eb2081d" + ); + await parameters.connect(multisig).set('0x4bfB2FA13097E5312B19585042FdbF3562dC8676', PARAM_FORCE_TRANSFER_ASSET_TO_OWNER_ON_LIQUIDATION, true); + await parameters.connect(multisig).set('0x988AAf8B36173Af7Ad3FEB36EfEc0988Fbd06d07', PARAM_FORCE_TRANSFER_ASSET_TO_OWNER_ON_LIQUIDATION, true); + + await parameters.connect(multisig).set(wrappedSlpUsdt.address, PARAM_FORCE_MOVE_WRAPPED_ASSET_POSITION_ON_LIQUIDATION, true); + + const auction = await attachContract('LiquidationAuction02', "0x9cCbb2F03184720Eef5f8fA768425AF06604Daf4") + console.log("liquidation auction: ", auction.address) + //////// end of auction //////////////////////////////////////////// + + //////// swappers //////////////////////////////////////////// + const deployment = await createSwappersDeployment({deployer: deployer.address}); + const deployed = await runDeployment(deployment, {deployer: deployer.address, verify: false}); + const swapperLp = await attachContract('SwapperUniswapV2Lp', deployed.SwapperUniswapV2Lp); + const swapperWeth = await attachContract('SwapperWethViaCurve', deployed.SwapperWethViaCurve); + await swappersRegistry.connect(multisig).add(swapperLp.address); + await swappersRegistry.connect(multisig).add(swapperWeth.address); + //////// end of auction //////////////////////////////////////////// + + + //////// wsslp simple case //////////////////////////////////////////// + // const sushi = await attachContract('IERC20', REWARD_TOKEN) + // + // console.log('-- check wslp') + // const balance = (await slpUsdt.balanceOf(TEST_WALLET)).toString(); + // console.log("balance of usdt sslp: ", balance) + // console.log("balance of sushi: ", (await sushi.balanceOf(TEST_WALLET)).toString()) + // + // await slpUsdt.connect(testWallet).approve(wrappedSlpUsdt.address, '1000000000000000000000'); + // await wrappedSlpUsdt.connect(testWallet).approve(VAULT, '1000000000000000000000'); + // await usdp.connect(testWallet).approve(cdpManager.address, '1000000000000000000000'); + // + // await cdpManager.connect(testWallet).wrapAndJoin(wrappedSlpUsdt.address, balance, '50000000000000000000'); + // + // console.log('claimable sushi: ', (await wrappedSlpUsdt.pendingReward(testWallet.address)).toString()) + // await network.provider.send("evm_mine"); + // await network.provider.send("evm_mine"); + // console.log('claimable sushi after 2 blocks: ', (await wrappedSlpUsdt.pendingReward(testWallet.address)).toString()) + // console.log('bones fees wallet balance before claim: ', (await sushi.balanceOf(MULTISIG_FEE)).toString()) + // await wrappedSlpUsdt.connect(testWallet).claimReward(testWallet.address) + // console.log("balance of sushi: ", (await sushi.balanceOf(TEST_WALLET)).toString()) + // console.log('sushi fees wallet balance after claim: ', (await sushi.balanceOf(MULTISIG_FEE)).toString()) + //////// end of wsslp simple case //////////////////////////////////////////// + + //////// wrapped asset leverage //////////////////////////////////////////// + // console.log('-- check simple leverage') + // const assetAmount = await slpUsdt.balanceOf(TEST_WALLET); + // const usdpAmount = ether('250'); // leverage >2 atm + // + // const wslpBalance1 = await wrappedSlpUsdt.balanceOf(TEST_WALLET); + // const slpBalance1 = await slpUsdt.balanceOf(TEST_WALLET); + // const usdpBalance1 = await usdp.balanceOf(TEST_WALLET); + // const vaultPosition1 = await vault.collaterals(wrappedSlpUsdt.address, TEST_WALLET); + // const debt1 = await vault.debts(wrappedSlpUsdt.address, TEST_WALLET); + // console.log(`balances before: slp ${weiToEther(slpBalance1)}, wslp ${weiToEther(wslpBalance1)}, usdp ${weiToEther(usdpBalance1)}`); + // console.log(`position before: ${weiToEther(vaultPosition1)}, debt: ${weiToEther(debt1)}`); + // + // await slpUsdt.connect(testWallet).approve(wrappedSlpUsdt.address, ether('5000')); // wrap + // await usdp.connect(testWallet).approve(cdpManager.address, ether('5000')); // borrow fee + // await wrappedSlpUsdt.connect(testWallet).approve(VAULT, ether('5000')); // borrow + // await usdp.connect(testWallet).approve(swapperLp.address, ether('5000')); // swap + // await slpUsdt.connect(testWallet).approve(swapperLp.address, ether('5000')); // swap back + // + // const predictedWeth = await swapperLp.predictAssetOut(slpUsdt.address, usdpAmount); + // await cdpManager.connect(testWallet).wrapAndJoinWithLeverage(wrappedSlpUsdt.address, swapperLp.address, assetAmount, usdpAmount, predictedWeth.mul(99).div(100)); + // + // const wslpBalance2 = await wrappedSlpUsdt.balanceOf(TEST_WALLET); + // const slpBalance2 = await slpUsdt.balanceOf(TEST_WALLET); + // const usdpBalance2 = await usdp.balanceOf(TEST_WALLET); + // const vaultPosition2 = await vault.collaterals(wrappedSlpUsdt.address, TEST_WALLET); + // const debt2 = await vault.debts(wrappedSlpUsdt.address, TEST_WALLET); + // console.log(`balances after: slp ${weiToEther(slpBalance2)}, wslp ${weiToEther(wslpBalance2)}, usdp ${weiToEther(usdpBalance2)}`); + // console.log(`position after: ${weiToEther(vaultPosition2)}, debt: ${weiToEther(debt2)}, diff: ${weiToEther(vaultPosition2.sub(vaultPosition1))}`); + // + // assert(slpBalance1.sub(assetAmount).eq(slpBalance2)) + // assert(wslpBalance2.eq(wslpBalance1)); + // assert(usdpBalance1.eq(usdpBalance2)); + // assert(vaultPosition2.sub(vaultPosition1).gt(assetAmount)); + // + // console.log('-- deleverage') + // const predictedUsdp = await swapperLp.predictUsdpOut(slpUsdt.address, assetAmount.div(2)); + // await cdpManager.connect(testWallet).unwrapAndExitWithDeleverage(wrappedSlpUsdt.address, swapperLp.address, assetAmount.div(2), assetAmount.div(2), predictedUsdp.mul(99).div(100)); + // + // const wslpBalance3 = await wrappedSlpUsdt.balanceOf(TEST_WALLET); + // const slpBalance3 = await slpUsdt.balanceOf(TEST_WALLET); + // const usdpBalance3 = await usdp.balanceOf(TEST_WALLET); + // const vaultPosition3 = await vault.collaterals(wrappedSlpUsdt.address, TEST_WALLET); + // const debt3 = await vault.debts(wrappedSlpUsdt.address, TEST_WALLET); + // console.log(`balances after: slp ${weiToEther(slpBalance3)}, wslp ${weiToEther(wslpBalance3)}, usdp ${weiToEther(usdpBalance3)}`); + // console.log(`position after: ${weiToEther(vaultPosition3)}, debt: ${weiToEther(debt3)}`); + // + // + // assert(slpBalance3.sub(slpBalance2).eq(assetAmount.div(2))); + // assert(usdpBalance3.eq(usdpBalance2) || usdpBalance3.sub(1).eq(usdpBalance2)); + // assert(vaultPosition3.add(assetAmount).eq(vaultPosition2)); + //////// end of wrapped asset leverage //////////////////////////////////////////// +} + + +deploy() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); + diff --git a/test/helpers/deploy.js b/test/helpers/deploy.js index 32065b3..622db3c 100644 --- a/test/helpers/deploy.js +++ b/test/helpers/deploy.js @@ -1,3 +1,4 @@ +const {createDeployment: createWrappedSLPDeployment} = require("../../lib/deployments/wrappedSLP"); const {createDeployment: createWrappedSSLPDeployment} = require("../../lib/deployments/wrappedSSLP"); const {createDeployment: createCoreDeployment} = require("../../lib/deployments/core"); const {runDeployment, loadHRE} = require("../helpers/deployUtils"); @@ -9,6 +10,7 @@ const {ORACLE_TYPE_CHAINLINK_MAIN_ASSET, ORACLE_TYPE_WRAPPED_TO_UNDERLYING, ORAC ORACLE_TYPE_BRIDGED_USDP, } = require("../../lib/constants"); const UniswapV2FactoryDeployCode = require("./UniswapV2DeployCode"); +const {ZERO_ADDRESS} = require("./deployUtils"); const EthersBN = ethers.BigNumber.from; const ether = ethers.utils.parseUnits; @@ -19,6 +21,8 @@ const BORROW_FEE_RECEIVER_ADDRESS = '0x0000000000000000000000000000000123456789' const SHIBA_TOPDOG_BONES_PER_BLOCK = ether("50"); const SHIBA_TOPDOG_DIRECT_BONES_USER_PERCENT = EthersBN("33"); +const SUSHI_MASTERCHEF_SUSHI_PER_BLOCK = ether("100"); + const CASE_WRAPPED_TO_UNDERLYING_CHAINLINK = 1; const CASE_WRAPPED_TO_UNDERLYING_WRAPPED_LP_TOKEN = 2; const CASE_WRAPPED_TO_UNDERLYING_WRAPPED_LP_TOKEN_KEYDONIX = 3; @@ -176,6 +180,63 @@ async function prepareWrappedSSLP(context, _oracleCase) { ); } +/** + * @param _oracleCase - case for oracle preparation, see CASE_* constants + * + */ +async function prepareWrappedSLP(context, _oracleCase) { + await prepareCoreContracts(context); // oracles will be prepared below + + context.tokenA = await deployContract("EmptyToken", 'TokenA descr', 'tokenA', 18, ether('100'), context.deployer.address); + context.tokenB = await deployContract("EmptyToken", 'TokenB descr', 'tokenB', 16, ether('100'), context.deployer.address); + context.tokenC = await deployContract("EmptyToken", 'TokenC descr', 'tokenC', 12, ether('100'), context.deployer.address); + context.tokenD = await deployContract("EmptyToken", 'TokenD descr', 'tokenD', 14, ether('100'), context.deployer.address); + + context.lpToken0 = await deployContract("SushiSwapLpToken_Mock", context.tokenA.address, context.tokenB.address, 'SushiSwap LP0', 'SSLP0', 18); + context.lpToken1 = await deployContract("SushiSwapLpToken_Mock", context.tokenC.address, context.tokenD.address, 'SushiSwap LP1', 'SSLP1', 17); + + context.rewardToken = await deployContract("SushiToken_Mock"); + context.rewardDistributor = await deployContract( + "MasterChef_Mock", + context.rewardToken.address, + "0x0000000000000000000000000000000000000001", + SUSHI_MASTERCHEF_SUSHI_PER_BLOCK, + 1, + 2, + ); + await context.rewardDistributor.add(50, context.lpToken0.address, false); + await context.rewardDistributor.add(50, context.lpToken1.address, false); + await context.rewardToken.transferOwnership(context.rewardDistributor.address); + + context.wslpFactory = await attachContract("WSLPFactory", (await deployWrappedSLP(context)).WSLPFactory); + + await context.wslpFactory.deploy(0); + context.wrappedSlp0 = await attachContract("WrappedSushiSwapLp", await context.wslpFactory.wrappedLpByPoolId(0)); + + await context.wslpFactory.deploy(1); + context.wrappedSlp1 = await attachContract("WrappedSushiSwapLp", await context.wslpFactory.wrappedLpByPoolId(1)) + + await prepareOracle(context, _oracleCase, { + collateral: context.wrappedSlp0, + collateralUnderlying: await attachContract('IERC20', await context.lpToken0.token0()), + collateralWrappedAssetUnderlying: context.lpToken0, + }); + + await context.vaultManagerParameters.setCollateral( + context.collateral.address, + '0', // stability fee // todo replace in tests for stability dee + '13', // liquidation fee + '75', // initial collateralization + '76', // liquidation ratio + '0', // liquidation discount (3 decimals) + '100', // devaluation period in blocks + ether('100000'), // debt limit + [context.collateralOracleType], // enabled oracles + 0, + 0, + ); +} + async function deployCore(context) { const deployment = await createCoreDeployment({ deployer: context.deployer.address, @@ -203,6 +264,19 @@ async function deployWrappedSSLP(context, topDogPoolId) { return await runDeployment(deployment, {hre, deployer: context.deployer.address}); } +async function deployWrappedSLP(context) { + const deployment = await createWrappedSLPDeployment({ + deployer: context.deployer.address, + manager: context.manager.address, + vaultParameters: context.vaultParameters.address, + rewardDistributor: context.rewardDistributor.address, + feeReceiver: ZERO_ADDRESS, + feePercent: 0 // todo must be set in tests for fee + }); + const hre = await loadHRE(); + return await runDeployment(deployment, {hre, deployer: context.deployer.address}); +} + /** * @param _oracleCase - case for oracle preparation, see CASE_* constants * @param params - values to overwrite context @@ -403,10 +477,13 @@ async function poolDeposit(context, uniswapRouter, token, tokenAmountToPool) { module.exports = { prepareCoreContracts, prepareWrappedSSLP, + prepareWrappedSLP, SHIBA_TOPDOG_BONES_PER_BLOCK, SHIBA_TOPDOG_DIRECT_BONES_USER_PERCENT, + SUSHI_MASTERCHEF_SUSHI_PER_BLOCK, + CASE_WRAPPED_TO_UNDERLYING_CHAINLINK, CASE_WRAPPED_TO_UNDERLYING_WRAPPED_LP_TOKEN, CASE_WRAPPED_TO_UNDERLYING_WRAPPED_LP_TOKEN_KEYDONIX, diff --git a/test/wrapped-assets/WrappedSushiSwapLp.test.js b/test/wrapped-assets/WrappedSushiSwapLp.test.js new file mode 100644 index 0000000..48f8bc7 --- /dev/null +++ b/test/wrapped-assets/WrappedSushiSwapLp.test.js @@ -0,0 +1,1001 @@ +const {expect} = require("chai"); +const {ethers} = require("hardhat"); +const { + prepareWrappedSLP, CASE_WRAPPED_TO_UNDERLYING_WRAPPED_LP_TOKEN, + CASE_WRAPPED_TO_UNDERLYING_WRAPPED_LP_TOKEN_KEYDONIX, +} = require("../helpers/deploy"); +const {deployContract, attachContract, ether, BN} = require("../helpers/ethersUtils"); +const {PARAM_FORCE_MOVE_WRAPPED_ASSET_POSITION_ON_LIQUIDATION} = require("../../lib/constants"); +const {cdpManagerWrapper} = require("../helpers/cdpManagerWrappers"); +const {ZERO_ADDRESS} = require("../helpers/deployUtils"); +const Abi = require('@ethersproject/abi'); +const {sushiReward} = require("./helpers/MasterChefLogic"); + +const EPSILON = BN('400000'); + +const oracleCases = [ + [CASE_WRAPPED_TO_UNDERLYING_WRAPPED_LP_TOKEN, 'cdp manager'], + [CASE_WRAPPED_TO_UNDERLYING_WRAPPED_LP_TOKEN_KEYDONIX, 'cdp manager keydonix'], +] + +let context = {}; +describe("WrappedSushiSwapLp", function () { + + beforeEach(async function () { + await network.provider.send("evm_setAutomine", [true]); + + [context.deployer, context.user1, context.user2, context.user3, context.manager, context.sushiFeeReceiver] = await ethers.getSigners(); + }); + + describe("factory", function () { + beforeEach(async function () { + await prepareWrappedSLP(context, CASE_WRAPPED_TO_UNDERLYING_WRAPPED_LP_TOKEN); + }) + + it('set fee for manager only', async function () { + await context.wslpFactory.setFee('0x0000000000000000000000000000000000000005', 10); + + await expect( + context.wslpFactory.connect(context.user3).setFee('0x0000000000000000000000000000000000000006', 11) + ).to.be.revertedWith("AUTH_FAILED"); + + expect(await context.wslpFactory.feeInfo()).deep.to.be.equal(['0x0000000000000000000000000000000000000005', 10]); + }) + + it('fee in range', async function () { + await context.wslpFactory.setFee('0x0000000000000000000000000000000000000000', 50); + await context.wslpFactory.setFee('0x0000000000000000000000000000000000000050', 0); + + await expect( + context.wslpFactory.setFee('0x0000000000000000000000000000000000000000', 51) + ).to.be.revertedWith("INVALID_FEE"); + + expect(await context.wslpFactory.feeInfo()).deep.to.be.equal(['0x0000000000000000000000000000000000000050', 0]); + }) + }); + + describe("Oracles independent tests", function () { + beforeEach(async function () { + await prepareWrappedSLP(context, CASE_WRAPPED_TO_UNDERLYING_WRAPPED_LP_TOKEN); + + // initials distribution of lptokens to users + await context.lpToken0.transfer(context.user1.address, ether('1')); + await context.lpToken0.transfer(context.user2.address, ether('1')); + await context.lpToken0.transfer(context.user3.address, ether('1')); + }) + + describe("constructor", function () { + it("wrapped token name and symbol", async function () { + expect(await context.wrappedSlp0.symbol()).to.be.equal("wuSSLP0tokenAtokenB"); // pool 0 with lpToken0 (tokenA, tokenB) + expect(await context.wrappedSlp0.name()).to.be.equal("Wrapped by Unit SushiSwap LP0 tokenA-tokenB"); // pool 1 with lpToken1 (tokenC, tokenD) + expect(await context.wrappedSlp0.decimals()).to.be.equal(await context.lpToken0.decimals()); + + expect(await context.wrappedSlp1.symbol()).to.be.equal("wuSSLP1tokenCtokenD"); // pool 1 with lpToken1 (tokenC, tokenD) + expect(await context.wrappedSlp1.name()).to.be.equal("Wrapped by Unit SushiSwap LP1 tokenC-tokenD"); // pool 1 with lpToken1 (tokenC, tokenD) + expect(await context.wrappedSlp1.decimals()).to.be.equal(await context.lpToken1.decimals()); + }); + + it("reinitialization is prohibited", async function () { + await expect( + context.wrappedSlp0.initialize(1) + ).to.be.revertedWith("Initializable: contract is already initialized"); + }); + }); + + describe("pool direct deposit/withdraw", function () { + it("prohibited deposit for another user", async function () { + const lockAmount = ether('0.4'); + await prepareUserForJoin(context.user1, ether('1')); + + await expect( + context.wrappedSlp0.connect(context.user1).deposit(context.user2.address, lockAmount) + ).to.be.revertedWith("AUTH_FAILED"); + }); + + it("not approved lp token", async function () { + const lockAmount = ether('0.4'); + await expect( + context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount) + ).to.be.revertedWith("TRANSFER_FROM_FAILED"); + }); + + it("zero deposit", async function () { + await expect( + context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, 0) + ).to.be.revertedWith("INVALID_AMOUNT"); + }); + + it("reinitialization", async function () { + const lockAmount = ether('0.4'); + await prepareUserForJoin(context.user1, ether('1')); + + await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + const user1ProxyAddr = await context.wrappedSlp0.usersProxies(context.user1.address); + const user1Proxy = await attachContract('WSLPUserProxy', user1ProxyAddr); + + // user cannot transfer tokens + await expect( + user1Proxy.initialize(1, context.user3.address) + ).to.be.revertedWith("ALREADY_INITIALIZED"); + }); + + it("transfer lp token", async function () { + const lockAmount = ether('0.4'); + await prepareUserForJoin(context.user1, ether('1')); + await prepareUserForJoin(context.user2, ether('1')); + + await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + await context.wrappedSlp0.connect(context.user2).deposit(context.user2.address, lockAmount); + + // user cannot transfer tokens + await expect( + context.wrappedSlp0.connect(context.user1).transfer(context.user3.address, lockAmount) + ).to.be.revertedWith("AUTH_FAILED"); + + // another user cannot transfer tokens + await expect( + context.wrappedSlp0.connect(context.user2).transfer(context.user3.address, lockAmount) + ).to.be.revertedWith("AUTH_FAILED"); + }); + + + it("transfer from for lp token", async function () { + const lockAmount = ether('0.4'); + await prepareUserForJoin(context.user1, ether('1')); + + await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + + // user cannot transfer tokens from another + await expect( + context.wrappedSlp0.connect(context.user2).transferFrom(context.user1.address, context.user3.address, lockAmount) + ).to.be.revertedWith("AUTH_FAILED"); + + // even if they are approved + await context.usdp.connect(context.user1).approve(context.user2.address, lockAmount); + await expect( + context.wrappedSlp0.connect(context.user2).transferFrom(context.user1.address, context.user3.address, lockAmount) + ).to.be.revertedWith("AUTH_FAILED"); + + // transfer allowance for vault tested in liquidation cases + }); + + it("simple deposit/withdraw", async function () { + const lockAmount = ether('0.4'); + await prepareUserForJoin(context.user1, ether('1')); + + await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6')); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(lockAmount); + + + await context.wrappedSlp0.connect(context.user1).withdraw(context.user1.address, lockAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1')); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0); + + expect(await rewardTokenBalance(context.user1)).to.be.equal(0); + await claimReward(context.user1) + expect(await rewardTokenBalance(context.user1)).not.to.be.equal(0); + }); + + it("separate state in pools", async function () { + const lockAmount = ether('0.4'); + + await context.lpToken1.transfer(context.user1.address, ether('1')); + + await context.lpToken0.connect(context.user1).approve(context.wrappedSlp0.address, lockAmount); + await context.lpToken1.connect(context.user1).approve(context.wrappedSlp1.address, lockAmount); + + const {blockNumber: deposit1Block} = await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6')); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(lockAmount); + + const {blockNumber: deposit2Block} = await context.wrappedSlp1.connect(context.user1).deposit(context.user1.address, lockAmount); + expect(await context.lpToken1.balanceOf(context.user1.address)).to.be.equal(ether('0.6')); + expect(await context.wrappedSlp1.balanceOf(context.user1.address)).to.be.equal(lockAmount); + + const user1ProxyAddr = await context.wrappedSlp0.usersProxies(context.user1.address); + const user1Proxy = await attachContract('WSLPUserProxy', user1ProxyAddr); + + const user1ProxyAddr2 = await context.wrappedSlp1.usersProxies(context.user1.address); + const user1Proxy2 = await attachContract('WSLPUserProxy', user1ProxyAddr2); + + const {blockNumber: withdraw2Block} = await context.wrappedSlp1.connect(context.user1).withdraw(context.user1.address, lockAmount); + expect(await context.lpToken1.balanceOf(context.user1.address)).to.be.equal(ether('1')); + expect(await context.wrappedSlp1.balanceOf(context.user1.address)).to.be.equal(0); + + const {blockNumber: withdraw1Block} = await context.wrappedSlp0.connect(context.user1).withdraw(context.user1.address, lockAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1')); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0); + + const user1Reward1 = sushiReward(deposit1Block, withdraw1Block); + expect(await rewardTokenBalance(user1Proxy)).to.be.closeTo(user1Reward1, EPSILON); + + const user1Reward2 = sushiReward(deposit2Block, withdraw2Block); + expect(await rewardTokenBalance(user1Proxy2)).to.be.closeTo(user1Reward2, EPSILON); + + await claimReward(context.user1) + expect(await rewardTokenBalance(context.user1)).to.be.closeTo(user1Reward1, EPSILON); + + await context.wrappedSlp1.connect(context.user1).claimReward(context.user1.address); + expect(await rewardTokenBalance(context.user1)).to.be.closeTo(user1Reward1.add(user1Reward2), EPSILON); + }); + + it("emergency withdraw + withdraw any token", async function () { + const lockAmount = ether('0.4'); + await prepareUserForJoin(context.user1, ether('1')); + + await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6')); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(lockAmount); + + await context.wrappedSlp0.connect(context.user1).emergencyWithdraw(); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6')); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0); + + await context.wrappedSlp0.connect(context.user1).withdrawToken(context.lpToken0.address, lockAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1')); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0); + + expect(await rewardTokenBalance(context.user1)).to.be.equal(0); + await claimReward(context.user1) + expect(await rewardTokenBalance(context.user1)).to.be.equal(0); + }); + + it("withdraw reward token from proxy (with fee)", async function () { + await context.wslpFactory.setFee(context.sushiFeeReceiver.address, 10); + + const lockAmount = ether('0.4'); + await prepareUserForJoin(context.user1, ether('1')); + + await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + const user1ProxyAddr = await context.wrappedSlp0.usersProxies(context.user1.address); + + // part of reward token balances + const proxyRewardTokenBalance1 = await rewardTokenBalance(user1ProxyAddr); + await context.wrappedSlp0.connect(context.user1).withdrawToken(context.rewardToken.address, proxyRewardTokenBalance1.div(2)); + const proxyRewardTokenBalance2 = await rewardTokenBalance(user1ProxyAddr); + const userRewardTokenBalance2 = await rewardTokenBalance(context.user1); + const feeReceiverRewardTokenBalance2 = await rewardTokenBalance(context.sushiFeeReceiver); + + expect(proxyRewardTokenBalance2).to.be.equal(proxyRewardTokenBalance1.div(2)); + expect(userRewardTokenBalance2).to.be.equal(proxyRewardTokenBalance1.div(2).mul(90).div(100)); + expect(feeReceiverRewardTokenBalance2).to.be.equal(proxyRewardTokenBalance1.div(2).mul(10).div(100)); + + // all + await context.wrappedSlp0.connect(context.user1).withdrawToken(context.rewardToken.address, proxyRewardTokenBalance2); + const proxyRewardTokenBalance3 = await rewardTokenBalance(user1ProxyAddr); + const userRewardTokenBalance3 = await rewardTokenBalance(context.user1); + const feeReceiverRewardTokenBalance3 = await rewardTokenBalance(context.sushiFeeReceiver); + + expect(proxyRewardTokenBalance3).to.be.equal(0); + expect(userRewardTokenBalance3).to.be.equal(proxyRewardTokenBalance1.mul(90).div(100)); + expect(feeReceiverRewardTokenBalance3).to.be.equal(proxyRewardTokenBalance1.mul(10).div(100)); + }); + + it("withdraw some token from proxy (with fee)", async function () { + await context.wslpFactory.setFee(context.sushiFeeReceiver.address, 10); + + const lockAmount = ether('0.4'); + await prepareUserForJoin(context.user1, ether('1')); + + await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + const user1ProxyAddr = await context.wrappedSlp0.usersProxies(context.user1.address); + + const amount = ether('1'); + await context.lpToken1.transfer(user1ProxyAddr, amount); + + // part of balances + await context.wrappedSlp0.connect(context.user1).withdrawToken(context.lpToken1.address, amount.div(2)); + const proxyBalance2 = await context.lpToken1.balanceOf(user1ProxyAddr); + const userBalance2 = await context.lpToken1.balanceOf(context.user1.address); + const feeReceiverBalance2 = await context.lpToken1.balanceOf(context.sushiFeeReceiver.address); + + expect(proxyBalance2).to.be.equal(amount.div(2)); + expect(userBalance2).to.be.equal(amount.div(2)); + expect(feeReceiverBalance2).to.be.equal(0); + + // all + await context.wrappedSlp0.connect(context.user1).withdrawToken(context.lpToken1.address, amount.div(2)); + const proxyBalance3 = await context.lpToken1.balanceOf(user1ProxyAddr); + const userBalance3 = await context.lpToken1.balanceOf(context.user1.address); + const feeReceiverBalance3 = await context.lpToken1.balanceOf(context.sushiFeeReceiver.address); + + expect(proxyBalance3).to.be.equal(0); + expect(userBalance3).to.be.equal(amount); + expect(feeReceiverBalance3).to.be.equal(0); + }); + + it("no emergency withdrawal allowed with unit position", async function () { + const lockAmount = ether('0.4'); + await prepareUserForJoin(context.user1, ether('1')); + + await wrapAndJoin(context.user1, lockAmount, 0); + + await expect( + context.wrappedSlp0.connect(context.user1).emergencyWithdraw() + ).to.be.revertedWith("burn amount exceeds balance"); + + await cdpManagerWrapper.exit(context, context.user1, context.wrappedSlp0, lockAmount.div(2), 0); + + await expect( + context.wrappedSlp0.connect(context.user1).emergencyWithdraw() + ).to.be.revertedWith("burn amount exceeds balance"); + + // full exit from unit, user got all wrapped tokens + await cdpManagerWrapper.exit(context, context.user1, context.wrappedSlp0, lockAmount.div(2), 0); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6')); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(ether('0.4')); + + await context.wrappedSlp0.connect(context.user1).emergencyWithdraw() + await context.wrappedSlp0.connect(context.user1).withdrawToken(context.lpToken0.address, lockAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1')); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0); + + }); + }); + + describe("proxies direct calls", function () { + it("only manager methods", async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, lockAmount); + await wrapAndJoin(context.user1, lockAmount, usdpAmount); + const user1ProxyAddr = await context.wrappedSlp0.usersProxies(context.user1.address); + expect(user1ProxyAddr).not.to.be.equal(ZERO_ADDRESS); + + const user1Proxy = await attachContract('WSLPUserProxy', user1ProxyAddr); + + await expect( + user1Proxy.approveLpToRewardDistributor(context.lpToken0.address) + ).to.be.revertedWith("AUTH_FAILED"); + + await expect( + user1Proxy.deposit(ether('1')) + ).to.be.revertedWith("AUTH_FAILED"); + + await expect( + user1Proxy.withdraw(context.lpToken0.address, ether('1'), context.user1.address) + ).to.be.revertedWith("AUTH_FAILED"); + + await expect( + user1Proxy.claimReward(context.user1.address) + ).to.be.revertedWith("AUTH_FAILED"); + + await expect( + user1Proxy.emergencyWithdraw() + ).to.be.revertedWith("AUTH_FAILED"); + + await expect( + user1Proxy.withdrawToken(context.lpToken0.address, context.user1.address, 100) + ).to.be.revertedWith("AUTH_FAILED"); + }); + }); + + describe("reward distribution cases", function () { + it('consecutive deposit and withdrawal with 3 users', async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, lockAmount); + await prepareUserForJoin(context.user2, lockAmount); + await prepareUserForJoin(context.user3, lockAmount); + + const {blockNumber: deposit1Block} = await wrapAndJoin(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "transferred from user"); + const {blockNumber: deposit2Block} = await wrapAndJoin(context.user2, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user2.address)).to.be.equal(ether('0.6'), "transferred from user"); + const {blockNumber: deposit3Block} = await wrapAndJoin(context.user3, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user3.address)).to.be.equal(ether('0.6'), "transferred from user"); + + expect(await context.lpToken0.balanceOf(context.rewardDistributor.address)).to.be.equal(lockAmount.mul(3), "transferred to reward distributor"); + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(lockAmount.mul(3), "wrapped token sent to vault"); + expect(await context.wrappedSlp0.totalSupply()).to.be.equal(lockAmount.mul(3), "minted only wrapped tokens for deposited amount"); + + const {blockNumber: withdrawal1Block} = await unwrapAndExit(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1'), "returned asset"); + const {blockNumber: withdrawal2Block} = await unwrapAndExit(context.user2, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1'), "returned asset"); + const {blockNumber: withdrawal3Block} = await unwrapAndExit(context.user3, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1'), "returned asset"); + + const user1Proxy = await context.wrappedSlp0.usersProxies(context.user1.address); + const user2Proxy = await context.wrappedSlp0.usersProxies(context.user2.address); + const user3Proxy = await context.wrappedSlp0.usersProxies(context.user3.address); + + // no reward sent to user on deposit/withdrawal + expect(await rewardTokenBalance(context.user1)).to.be.equal(0); + expect(await rewardTokenBalance(context.user2)).to.be.equal(0); + expect(await rewardTokenBalance(context.user3)).to.be.equal(0); + + // but reward was sent to user proxies by reward distributor + // with 3 simultaneous users we have imprecise calculations + const user1Reward = sushiReward(deposit1Block, deposit2Block) + .add(sushiReward(deposit2Block, deposit3Block).div(2)) + .add(sushiReward(deposit3Block, withdrawal1Block).div(3)); + expect(await rewardTokenBalance(user1Proxy)).to.be.closeTo(user1Reward, EPSILON); + + const user2Reward = sushiReward(deposit2Block, deposit3Block).div(2) + .add(sushiReward(deposit3Block, withdrawal1Block).div(3)) + .add(sushiReward(withdrawal1Block, withdrawal2Block).div(2)); + expect(await rewardTokenBalance(user2Proxy)).to.be.closeTo(user2Reward, EPSILON); + + const user3Reward = sushiReward(deposit3Block, withdrawal1Block).div(3) + .add(sushiReward(withdrawal1Block, withdrawal2Block).div(2)) + .add(sushiReward(withdrawal2Block, withdrawal3Block).div(1)); + expect(await rewardTokenBalance(user3Proxy)).to.be.closeTo(user3Reward, EPSILON); + + await claimReward(context.user1) + expect(await rewardTokenBalance(context.user1)).to.be.closeTo(user1Reward, EPSILON); + expect(await rewardTokenBalance(context.user2)).to.be.equal(0); + expect(await rewardTokenBalance(context.user3)).to.be.equal(0); + + await claimReward(context.user2) + expect(await rewardTokenBalance(context.user1)).to.be.closeTo(user1Reward, EPSILON); + expect(await rewardTokenBalance(context.user2)).to.be.closeTo(user2Reward, EPSILON); + expect(await rewardTokenBalance(context.user3)).to.be.equal(0); + + await claimReward(context.user3) + expect(await rewardTokenBalance(context.user1)).to.be.closeTo(user1Reward, EPSILON); + expect(await rewardTokenBalance(context.user2)).to.be.closeTo(user2Reward, EPSILON); + expect(await rewardTokenBalance(context.user3)).to.be.closeTo(user3Reward, EPSILON); + + expect(await rewardTokenBalance(user1Proxy)).to.be.equal(0); + expect(await rewardTokenBalance(user2Proxy)).to.be.equal(0); + expect(await rewardTokenBalance(user3Proxy)).to.be.equal(0); + }) + + it('reward tokens distribution with non consecutive deposit and withdrawal with 3 users', async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, lockAmount); + await prepareUserForJoin(context.user2, lockAmount); + await prepareUserForJoin(context.user3, lockAmount); + + const {blockNumber: deposit1Block} = await wrapAndJoin(context.user1, lockAmount, usdpAmount); + const {blockNumber: deposit2Block} = await wrapAndJoin(context.user2, lockAmount, usdpAmount); + + const {blockNumber: withdrawal1Block} = await unwrapAndExit(context.user1, lockAmount, usdpAmount); + const {blockNumber: deposit3Block} = await wrapAndJoin(context.user3, lockAmount, usdpAmount); + + const {blockNumber: withdrawal2Block} = await unwrapAndExit(context.user2, lockAmount, usdpAmount); + const {blockNumber: withdrawal3Block} = await unwrapAndExit(context.user3, lockAmount, usdpAmount); + + const user1Proxy = await context.wrappedSlp0.usersProxies(context.user1.address); + const user2Proxy = await context.wrappedSlp0.usersProxies(context.user2.address); + const user3Proxy = await context.wrappedSlp0.usersProxies(context.user3.address); + + // no reward sent to user on deposit/withdrawal + expect(await rewardTokenBalance(context.user1)).to.be.equal(0); + expect(await rewardTokenBalance(context.user2)).to.be.equal(0); + expect(await rewardTokenBalance(context.user3)).to.be.equal(0); + + const user1Reward = sushiReward(deposit1Block, deposit2Block) + .add(sushiReward(deposit2Block, withdrawal1Block).div(2)); + expect(await rewardTokenBalance(user1Proxy)).to.be.equal(user1Reward); + + const user2Reward = sushiReward(deposit2Block, withdrawal1Block).div(2) + .add(sushiReward(withdrawal1Block, deposit3Block)) + .add(sushiReward(deposit3Block, withdrawal2Block).div(2)); + expect(await rewardTokenBalance(user2Proxy)).to.be.equal(user2Reward); + + const user3Reward = sushiReward(deposit3Block, withdrawal2Block).div(2) + .add(sushiReward(withdrawal2Block, withdrawal3Block)); + expect(await rewardTokenBalance(user3Proxy)).to.be.equal(user3Reward); + + await claimReward(context.user1) + expect(await rewardTokenBalance(context.user1)).to.be.closeTo(user1Reward, EPSILON); + expect(await rewardTokenBalance(context.user2)).to.be.equal(0); + expect(await rewardTokenBalance(context.user3)).to.be.equal(0); + + await claimReward(context.user2) + expect(await rewardTokenBalance(context.user1)).to.be.closeTo(user1Reward, EPSILON); + expect(await rewardTokenBalance(context.user2)).to.be.closeTo(user2Reward, EPSILON); + expect(await rewardTokenBalance(context.user3)).to.be.equal(0); + + await claimReward(context.user3) + expect(await rewardTokenBalance(context.user1)).to.be.closeTo(user1Reward, EPSILON); + expect(await rewardTokenBalance(context.user2)).to.be.closeTo(user2Reward, EPSILON); + expect(await rewardTokenBalance(context.user3)).to.be.closeTo(user3Reward, EPSILON); + + expect(await rewardTokenBalance(user1Proxy)).to.be.equal(0); + expect(await rewardTokenBalance(user2Proxy)).to.be.equal(0); + expect(await rewardTokenBalance(user3Proxy)).to.be.equal(0); + }) + + it('simple case for pending reward', async function () { + const lockAmount = ether('0.2'); + const usdpAmount = ether('50'); + + await prepareUserForJoin(context.user1, lockAmount); + + expect(await pendingReward(context.user1)).to.be.equal(0); + const {blockNumber: block1} = await wrapAndJoin(context.user1, lockAmount, usdpAmount); + expect(await pendingReward(context.user1)).to.be.equal(0); + + await network.provider.send("evm_mine"); + const block2 = (await ethers.provider.getBlock("latest")).number + expect(await pendingReward(context.user1)).to.be.equal(sushiReward(block1, block2)); + + await network.provider.send("evm_mine"); + const block3 = (await ethers.provider.getBlock("latest")).number + expect(await pendingReward(context.user1)).to.be.equal(sushiReward(block1, block3)); + + const {blockNumber: block4} = await unwrapAndExit(context.user1, lockAmount, usdpAmount); + expect(await pendingReward(context.user1)).to.be.equal(sushiReward(block1, block4)); + + await claimReward(context.user1) + expect(await pendingReward(context.user1)).to.be.equal(0); + expect(await rewardTokenBalance(context.user1)).to.be.equal(sushiReward(block1, block4)); + }) + + it('reward tokens fee', async function () { + await context.wslpFactory.setFee(context.sushiFeeReceiver.address, 10); + + const lockAmount = ether('0.2'); + const usdpAmount = ether('50'); + + await prepareUserForJoin(context.user1, lockAmount); + + expect(await pendingReward(context.user1)).to.be.equal(0); + const {blockNumber: block1} = await wrapAndJoin(context.user1, lockAmount, usdpAmount); + expect(await pendingReward(context.user1)).to.be.equal(0); + + await network.provider.send("evm_mine"); + const block2 = (await ethers.provider.getBlock("latest")).number + expect(await pendingReward(context.user1)).to.be.equal(sushiReward(block1, block2).mul(90).div(100)); + + await network.provider.send("evm_mine"); + const block3 = (await ethers.provider.getBlock("latest")).number + expect(await pendingReward(context.user1)).to.be.equal(sushiReward(block1, block3).mul(90).div(100)); + + const {blockNumber: block4} = await unwrapAndExit(context.user1, lockAmount, usdpAmount); + expect(await pendingReward(context.user1)).to.be.equal(sushiReward(block1, block4).mul(90).div(100)); + + await claimReward(context.user1) + expect(await pendingReward(context.user1)).to.be.equal(0); + expect(await rewardTokenBalance(context.user1)).to.be.equal(sushiReward(block1, block4).mul(90).div(100)); + expect(await rewardTokenBalance(context.sushiFeeReceiver)).to.be.equal(sushiReward(block1, block4).mul(10).div(100)); + }) + + it('reward tokens fee with empty receiver', async function () { + await context.wslpFactory.setFee(ZERO_ADDRESS, 10); + + const lockAmount = ether('0.2'); + const usdpAmount = ether('50'); + + await prepareUserForJoin(context.user1, lockAmount); + + expect(await pendingReward(context.user1)).to.be.equal(0); + const {blockNumber: block1} = await wrapAndJoin(context.user1, lockAmount, usdpAmount); + expect(await pendingReward(context.user1)).to.be.equal(0); + + const {blockNumber: block2} = await unwrapAndExit(context.user1, lockAmount, usdpAmount); + expect(await pendingReward(context.user1)).to.be.equal(sushiReward(block1, block2)); + + await claimReward(context.user1) + expect(await pendingReward(context.user1)).to.be.equal(0); + expect(await rewardTokenBalance(context.user1)).to.be.equal(sushiReward(block1, block2)); + expect(await rewardTokenBalance(context.sushiFeeReceiver)).to.be.equal(0); + }) + + }); + + describe("reward distributor edge cases", function () { + it('handle change of lptopken in reward distributor', async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, ether('1')); + + context.migratorChef = await deployContract("MigratorChef_Mock"); + await context.migratorChef.setNewToken(context.lpToken1.address); + await context.lpToken1.transfer(context.migratorChef.address, ether('100')); + await context.rewardDistributor.setMigrator(context.migratorChef.address); + + await wrapAndJoin(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "rest of old lp tokens"); + expect(await context.lpToken1.balanceOf(context.user1.address)).to.be.equal(ether('0'), "no user balance in new lp token"); + expect(await context.lpToken0.balanceOf(context.rewardDistributor.address)).to.be.equal(lockAmount, "old lp tokens sent to reward distributor"); + expect(await context.lpToken1.balanceOf(context.rewardDistributor.address)).to.be.equal(ether('0'), "no reward distributor balance in new lp token"); + + expect(await context.wrappedSlp0.getUnderlyingToken()).to.be.equal(context.lpToken0.address); + await context.rewardDistributor.connect(context.user3).migrate(0); // anyone can migrate + expect(await context.wrappedSlp0.getUnderlyingToken()).to.be.equal(context.lpToken1.address); + + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "rest of old lp tokens"); + expect(await context.lpToken1.balanceOf(context.user1.address)).to.be.equal(ether('0'), "no user balance in new lp token"); + expect(await context.lpToken0.balanceOf(context.rewardDistributor.address)).to.be.equal(lockAmount, "old lp tokens sent to reward distributor"); + expect(await context.lpToken1.balanceOf(context.rewardDistributor.address)).to.be.equal(lockAmount, "reward distributor balance in new lp tokens"); + + await unwrapAndExit(context.user1, lockAmount, usdpAmount); + + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "nothing returned in old lp tokens"); + expect(await context.lpToken1.balanceOf(context.user1.address)).to.be.equal(lockAmount, "but returned in new lp tokens"); + expect(await context.lpToken0.balanceOf(context.rewardDistributor.address)).to.be.equal(lockAmount, "old lp tokens didn't use"); + expect(await context.lpToken1.balanceOf(context.rewardDistributor.address)).to.be.equal(ether('0'), "no new lp tokens left in reward distributor"); + + // reapprove new lp to pool + await context.lpToken1.connect(context.user1).approve(context.wrappedSlp0.address, ether('1')); + // revert since new lp is not approved for reward distributor + await expect( + wrapAndJoin(context.user1, lockAmount, usdpAmount) + ).to.be.revertedWith("ERC20: transfer amount exceeds allowance"); + + await context.wrappedSlp0.connect(context.user1).approveLpToRewardDistributor(); + await wrapAndJoin(context.user1, lockAmount, usdpAmount); // success + }) + }); + + describe("liquidations related", function () { + it('move position', async function () { + const lockAmount = ether('0.4'); + + await prepareUserForJoin(context.user1, lockAmount); + + await context.vaultParameters.connect(context.deployer).setVaultAccess(context.deployer.address, true); + + const {blockNumber: block1} = await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + const {blockNumber: block2} = await context.wrappedSlp0.connect(context.deployer).movePosition(context.user1.address, context.user2.address, ether('0.1')); + + const user1Proxy = await context.wrappedSlp0.usersProxies(context.user1.address); + const user2Proxy = await context.wrappedSlp0.usersProxies(context.user2.address); + expect(user2Proxy).not.to.be.equal(ZERO_ADDRESS); // created on movePosition + + expect(await rewardTokenBalance(user1Proxy)).to.be.equal(sushiReward(block1, block2)); // sent to proxy + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6')); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(ether('0.4'), 'no wrapped tokens transferred'); + + expect(await rewardTokenBalance(context.user2)).to.be.equal(0); + expect(await context.lpToken0.balanceOf(context.user2.address)).to.be.equal(ether('1')); + expect(await context.wrappedSlp0.balanceOf(context.user2.address)).to.be.equal(0, 'no wrapped tokens transferred'); + + // next movePosition is senselessly since contract is in inconsistent state + }) + + it('move position to the same user', async function () { + const lockAmount = ether('0.4'); + + await prepareUserForJoin(context.user1, lockAmount); + await prepareUserForJoin(context.user2, lockAmount); + + await context.vaultParameters.connect(context.deployer).setVaultAccess(context.deployer.address, true); + + const {blockNumber: block1} = await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, ether('0.1')); + const {blockNumber: block2} = await context.wrappedSlp0.connect(context.user2).deposit(context.user2.address, lockAmount); + const {blockNumber: block3} = await context.wrappedSlp0.connect(context.deployer).movePosition(context.user2.address, context.user2.address, ether('0.2')); + + const user1Proxy = await context.wrappedSlp0.usersProxies(context.user1.address); + const user2Proxy = await context.wrappedSlp0.usersProxies(context.user2.address); + + expect(await rewardTokenBalance(user1Proxy)).to.be.equal(0); + expect(await rewardTokenBalance(user2Proxy)).to.be.equal(0); + + expect(await context.lpToken0.balanceOf(context.user2.address)).to.be.equal(ether('0.6')); + expect(await context.wrappedSlp0.balanceOf(context.user2.address)).to.be.equal(lockAmount, 'no wrapped tokens transferred'); + + const {blockNumber: block4} = await context.wrappedSlp0.connect(context.deployer).movePosition(context.user2.address, context.user2.address, lockAmount); + // nothing changes + expect(await rewardTokenBalance(user2Proxy)).to.be.equal(0); + expect(await context.lpToken0.balanceOf(context.user2.address)).to.be.equal(ether('0.6')); + expect(await context.wrappedSlp0.balanceOf(context.user2.address)).to.be.equal(lockAmount, 'no wrapped tokens transferred'); + + const {blockNumber: block5} = await context.wrappedSlp0.connect(context.user2).withdraw(context.user2.address, lockAmount); // can withdraw + + expect(await rewardTokenBalance(user2Proxy)).to.be.equal(sushiReward(block2, block5).mul(4).div(5)) + expect(await context.lpToken0.balanceOf(context.user2.address)).to.be.equal(ether('1')); + expect(await context.wrappedSlp0.balanceOf(context.user2.address)).to.be.equal(0, 'wrapped tokens burned'); + }) + + it('liquidation (with moving position)', async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, lockAmount); + await context.assetsBooleanParameters.set(context.wrappedSlp0.address, PARAM_FORCE_MOVE_WRAPPED_ASSET_POSITION_ON_LIQUIDATION, true); + await context.usdp.tests_mint(context.user2.address, usdpAmount.mul(2)); + await context.usdp.connect(context.user2).approve(context.vault.address, usdpAmount.mul(2)); + + await wrapAndJoin(context.user1, lockAmount, usdpAmount); + + await context.vaultManagerParameters.setInitialCollateralRatio(context.wrappedSlp0.address, BN(9)); + await context.vaultManagerParameters.setLiquidationRatio(context.wrappedSlp0.address, BN(10)); + + + await cdpManagerWrapper.triggerLiquidation(context, context.wrappedSlp0, context.user1); + + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(lockAmount, "wrapped tokens in vault"); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0); + expect(await context.wrappedSlp0.balanceOf(context.user2.address)).to.be.equal(0); + + await mineBlocks(10) + + await context.liquidationAuction.connect(context.user2).buyout(context.wrappedSlp0.address, context.user1.address); + + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(0, "wrapped tokens not in vault"); + let ownerCollateralAmount = await context.wrappedSlp0.balanceOf(context.user1.address); + let liquidatorCollateralAmount = await context.wrappedSlp0.balanceOf(context.user2.address); + expect(lockAmount).to.be.equal(ownerCollateralAmount.add(liquidatorCollateralAmount)) + + await context.wrappedSlp0.connect(context.user1).withdraw(context.user1.address, ownerCollateralAmount); + await context.wrappedSlp0.connect(context.user2).withdraw(context.user2.address, liquidatorCollateralAmount); + + expect(await context.wrappedSlp0.totalSupply()).to.be.equal(0); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ownerCollateralAmount.add(ether('0.6')), 'withdrawn all tokens'); + expect(await context.lpToken0.balanceOf(context.user2.address)).to.be.equal(liquidatorCollateralAmount.add(ether('1')), 'withdrawn all tokens'); + }) + + it('liquidation by owner (with moving position)', async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, lockAmount); + await context.assetsBooleanParameters.set(context.wrappedSlp0.address, PARAM_FORCE_MOVE_WRAPPED_ASSET_POSITION_ON_LIQUIDATION, true); + await context.usdp.connect(context.user1).approve(context.vault.address, usdpAmount.mul(2)); + + await wrapAndJoin(context.user1, lockAmount, usdpAmount); + + await context.vaultManagerParameters.setInitialCollateralRatio(context.wrappedSlp0.address, BN(9)); + await context.vaultManagerParameters.setLiquidationRatio(context.wrappedSlp0.address, BN(10)); + + await cdpManagerWrapper.triggerLiquidation(context, context.wrappedSlp0, context.user1); + + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(lockAmount, "wrapped tokens in vault"); + expect(await context.vault.collaterals(context.wrappedSlp0.address, context.user1.address)).to.be.equal(lockAmount); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0); + + await mineBlocks(52); + + await context.liquidationAuction.connect(context.user1).buyout(context.wrappedSlp0.address, context.user1.address); + + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(0, "wrapped tokens not in vault"); + expect(await context.vault.collaterals(context.wrappedSlp0.address, context.user1.address)).to.be.equal(0); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(lockAmount); + + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(lockAmount, 'tokens = position'); + + await context.wrappedSlp0.connect(context.user1).withdraw(context.user1.address, lockAmount); + + expect(await context.wrappedSlp0.totalSupply()).to.be.equal(0); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1'), 'withdrawn all tokens'); + + expect(await context.usdp.balanceOf(context.user1.address)).to.be.not.equal(ether('0'), 'user1 with collateral and usdp :pokerface:'); + }) + }); + }) + + oracleCases.forEach(params => { + describe(`Oracles dependent tests with ${params[1]}`, function () { + beforeEach(async function () { + await prepareWrappedSLP(context, params[0]); + + // initials distribution of lptokens to users + await context.lpToken0.transfer(context.user1.address, ether('1')); + await context.lpToken0.transfer(context.user2.address, ether('1')); + await context.lpToken0.transfer(context.user3.address, ether('1')); + }) + + describe("join/exit cases via cdp manager", function () { + [1, 10].forEach(blockInterval => + it(`simple deposit and withdrawal with interval ${blockInterval} blocks`, async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, lockAmount); + + const {blockNumber: depositBlock} = await wrapAndJoin(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "transferred from user"); + expect(await context.usdp.balanceOf(context.user1.address)).to.be.equal(usdpAmount, "got usdp"); + expect(await context.lpToken0.balanceOf(context.rewardDistributor.address)).to.be.equal(lockAmount, "transferred to reward distributor"); + expect(await context.lpToken0.balanceOf(context.wrappedSlp0.address)).to.be.equal(ether('0'), "transferred not to pool"); + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(lockAmount, "wrapped token sent to vault"); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0, "wrapped token sent not to user"); + expect(await context.wrappedSlp0.totalSupply()).to.be.equal(lockAmount, "minted only wrapped tokens for deposited amount"); + + for (let i = 0; i < blockInterval - 1; ++i) { + await network.provider.send("evm_mine"); + } + + const {blockNumber: withdrawalBlock} = await unwrapAndExit(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1'), "returned all tokens to user"); + expect(await context.usdp.balanceOf(context.user1.address)).to.be.equal(0, "returned usdp"); + expect(await context.lpToken0.balanceOf(context.rewardDistributor.address)).to.be.equal(0, "everything were withdrawn from reward distributor"); + expect(await context.lpToken0.balanceOf(context.wrappedSlp0.address)).to.be.equal(ether('0'), "withdrawn not to pool"); + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(0, "wrapped token withdrawn from vault"); + expect(await context.wrappedSlp0.totalSupply()).to.be.equal(0, "everything were burned"); + + await claimReward(context.user1); + expect(await rewardTokenBalance(context.user1)).to.be.equal(sushiReward(depositBlock, withdrawalBlock), "reward tokens reward got"); + }) + ); + + it(`simple deposit in one block`, async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, lockAmount); + + await network.provider.send("evm_setAutomine", [false]); + const joinTx = await wrapAndJoin(context.user1, lockAmount, usdpAmount); + const exitTx = await unwrapAndExit(context.user1, lockAmount, usdpAmount); + await network.provider.send("evm_mine"); + await network.provider.send("evm_setAutomine", [true]); + + const joinResult = await joinTx.wait(); + const exitResult = await exitTx.wait(); + expect(joinResult.blockNumber).to.be.equal(exitResult.blockNumber); + expect(joinResult.blockNumber).not.to.be.equal(null); + + expect(joinTx).to.emit(cdpManagerWrapper.cdpManager(context), "Join").withArgs(context.wrappedSlp0.address, context.user1.address, lockAmount, usdpAmount); + expect(exitTx).to.emit(cdpManagerWrapper.cdpManager(context), "Exit").withArgs(context.wrappedSlp0.address, context.user1.address, lockAmount, usdpAmount); + + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1'), "returned all tokens to user"); + expect(await context.lpToken0.balanceOf(context.rewardDistributor.address)).to.be.equal(0, "everything were withdrawn from reward distributor"); + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(0, "wrapped token withdrawn from vault"); + expect(await context.wrappedSlp0.totalSupply()).to.be.equal(0, "everything were burned"); + + await claimReward(context.user1); + expect(await rewardTokenBalance(context.user1)).to.be.equal(0, "reward tokens reward got"); + }); + + it(`simple case with several deposits`, async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, lockAmount); + + const {blockNumber: deposit1Block} = await wrapAndJoin(context.user1, lockAmount.div(2), usdpAmount.div(2)); + const {blockNumber: deposit2Block} = await wrapAndJoin(context.user1, lockAmount.div(2), usdpAmount.div(2)); + + const user1Proxy = await context.wrappedSlp0.usersProxies(context.user1.address); + + const reward = sushiReward(deposit1Block, deposit2Block); + expect(await rewardTokenBalance(user1Proxy)).to.be.equal(reward, "reward tokens reward got to proxy"); + + const {blockNumber: withdrawalBlock} = await unwrapAndExit(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1'), "returned all tokens to user"); + expect(await context.lpToken0.balanceOf(context.rewardDistributor.address)).to.be.equal(0, "everything were withdrawn from reward distributor"); + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(0, "wrapped token withdrawn from vault"); + expect(await context.wrappedSlp0.totalSupply()).to.be.equal(0, "everything were burned"); + + await claimReward(context.user1); + expect(await rewardTokenBalance(context.user1)).to.be.equal(sushiReward(deposit2Block, withdrawalBlock).add(reward), "reward tokens reward got"); + }) + + it(`simple case with target repayment`, async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, lockAmount); + await context.usdp.connect(context.user1).approve(context.vault.address, ether('1')); + + await wrapAndJoin(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "sent tokens"); + expect(await context.usdp.balanceOf(context.user1.address)).to.be.equal(usdpAmount, "got usdp"); + + await cdpManagerWrapper.unwrapAndExitTargetRepayment(context, context.user1, context.wrappedSlp0, lockAmount.div(2), usdpAmount.div(2)); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.8'), "returned tokens to user"); + expect(await context.usdp.balanceOf(context.user1.address)).to.be.equal(usdpAmount.div(2), "borrowed usdp without repaid"); + }) + + it(`mint usdp only without adding collateral`, async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('50'); + + await prepareUserForJoin(context.user1, lockAmount); + + await wrapAndJoin(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "sent tokens"); + expect(await context.usdp.balanceOf(context.user1.address)).to.be.equal(usdpAmount, "got usdp"); + + await wrapAndJoin(context.user1, 0, usdpAmount); + expect(await context.usdp.balanceOf(context.user1.address)).to.be.equal(usdpAmount.mul(2), "got usdp"); + }); + + it(`burn usdp without withdraw collateral`, async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, lockAmount); + + await wrapAndJoin(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "sent tokens"); + expect(await context.usdp.balanceOf(context.user1.address)).to.be.equal(usdpAmount, "got usdp"); + + await unwrapAndExit(context.user1, 0, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "tokens still locked"); + expect(await context.usdp.balanceOf(context.user1.address)).to.be.equal(0, "no usdp"); + }); + }); + + describe("cdp manager and direct wraps/unwraps", function () { + it('exit without unwrap', async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, ether('1')); + + await wrapAndJoin(context.user1, lockAmount, usdpAmount); + await cdpManagerWrapper.exit(context, context.user1, context.wrappedSlp0, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "nothing returned in lp tokens"); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(lockAmount, "but returned in new wrapped tokens"); + + // rescue of tokens - rejoin and unwrapAndExit + await cdpManagerWrapper.join(context, context.user1, context.wrappedSlp0, lockAmount, usdpAmount); + await unwrapAndExit(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1'), "everything returned in lp tokens"); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0, "zero wraped tokens"); + }) + + it('exit without unwrap with direct unwrap', async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, ether('1')); + + await wrapAndJoin(context.user1, lockAmount, usdpAmount); + await cdpManagerWrapper.exit(context, context.user1, context.wrappedSlp0, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "nothing returned in lp tokens"); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(lockAmount, "but returned in new wrapped tokens"); + + await context.wrappedSlp0.connect(context.user1).withdraw(context.user1.address, lockAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1'), "everything returned in lp tokens"); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0, "zero wrapped tokens"); + }) + + it('manual wrap and join and exit with cdp manager', async function () { + const lockAmount = ether('0.4'); + const usdpAmount = ether('100'); + + await prepareUserForJoin(context.user1, ether('1')); + + await context.wrappedSlp0.connect(context.user1).deposit(context.user1.address, lockAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('0.6'), "send lp tokens"); + expect(await context.lpToken0.balanceOf(context.rewardDistributor.address)).to.be.equal(ether('0.4'), "to reward distributor"); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(ether('0.4'), "got wrapped lp tokens"); + + await cdpManagerWrapper.join(context, context.user1, context.wrappedSlp0, lockAmount, usdpAmount); + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(ether('0.4'), "sent wrapped tokens to vault"); + + await unwrapAndExit(context.user1, lockAmount, usdpAmount); + expect(await context.lpToken0.balanceOf(context.user1.address)).to.be.equal(ether('1'), "everything returned in lp tokens"); + expect(await context.wrappedSlp0.balanceOf(context.user1.address)).to.be.equal(0, "zero wrapped tokens"); + expect(await context.wrappedSlp0.balanceOf(context.vault.address)).to.be.equal(0, "zero wrapped tokens"); + }) + }); + }) + }) +}); + +async function prepareUserForJoin(user, amount) { + await context.lpToken0.connect(user).approve(context.wrappedSlp0.address, amount); + await context.wrappedSlp0.connect(user).approve(context.vault.address, amount); +} + +async function pendingReward(user) { + return await context.wrappedSlp0.pendingReward(user.address) +} + +async function claimReward(user) { + return await context.wrappedSlp0.connect(user).claimReward(user.address) +} + +async function wrapAndJoin(user, assetAmount, usdpAmount) { + return cdpManagerWrapper.wrapAndJoin(context, user, context.wrappedSlp0, assetAmount, usdpAmount); +} + +async function unwrapAndExit(user, assetAmount, usdpAmount) { + return cdpManagerWrapper.unwrapAndExit(context, user, context.wrappedSlp0, assetAmount, usdpAmount); +} + +async function rewardTokenBalance(user) { + return await context.rewardToken.balanceOf(user.address ? user.address : user) +} + +async function mineBlocks(count) { + for (let i = 0; i < count; ++i) { + await network.provider.send("evm_mine"); + } +} \ No newline at end of file diff --git a/test/wrapped-assets/helpers/MasterChefLogic.js b/test/wrapped-assets/helpers/MasterChefLogic.js new file mode 100644 index 0000000..093147f --- /dev/null +++ b/test/wrapped-assets/helpers/MasterChefLogic.js @@ -0,0 +1,14 @@ +const {ethers} = require("ethers"); +const {SUSHI_MASTERCHEF_SUSHI_PER_BLOCK} = require("../../helpers/deploy"); +const {BN} = require("../../helpers/ethersUtils"); + +function sushiReward(startBlock, endBlock) { + return SUSHI_MASTERCHEF_SUSHI_PER_BLOCK + .mul(endBlock-startBlock) + .div(2) // 2 pools, divided between them + ; +} + +module.exports = { + sushiReward, +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 0eee297..4967244 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2151,6 +2151,11 @@ widest-line "^3.1.0" wrap-ansi "^4.0.0" +"@openzeppelin/contracts-upgradeable@^3.4.0": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-3.4.2.tgz#2c2a1b0fa748235a1f495b6489349776365c51b3" + integrity sha512-mDlBS17ymb2wpaLcrqRYdnBAmP1EwqhOXMvqWk2c5Q1N1pm5TkiCtXM9Xzznh4bYsQBq0aIWEkFFE2+iLSN1Tw== + "@openzeppelin/contracts@3.4.0": version "3.4.0" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.0.tgz#9a1669ad5f9fdfb6e273bb5a4fed10cb4cc35eb0"