diff --git a/.prettierignore b/.prettierignore index af534eca..02762160 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,5 +5,7 @@ coverage* gasReporterOutput.json lib/ contracts/* +!contracts/psm/ scripts/* test/* +!test/psm/ diff --git a/audits/blocksec_psm_241122.pdf b/audits/blocksec_psm_241122.pdf new file mode 100644 index 00000000..5dde0bfc Binary files /dev/null and b/audits/blocksec_psm_241122.pdf differ diff --git a/audits/salus_PSM_241122.pdf b/audits/salus_PSM_241122.pdf new file mode 100644 index 00000000..ae29daa2 Binary files /dev/null and b/audits/salus_PSM_241122.pdf differ diff --git a/contracts/hMath.sol b/contracts/hMath.sol index fb96ea20..038dbb52 100644 --- a/contracts/hMath.sol +++ b/contracts/hMath.sol @@ -28,4 +28,12 @@ library hMath { } } } + + function rmul(uint x, uint y) internal pure returns (uint z) { + unchecked { + z = x * y; + require(y == 0 || z / y == x); + z = z / hMath.ONE; + } + } } \ No newline at end of file diff --git a/contracts/interfaces/HayLike.sol b/contracts/interfaces/HayLike.sol index 0edf9337..edc2e207 100644 --- a/contracts/interfaces/HayLike.sol +++ b/contracts/interfaces/HayLike.sol @@ -8,4 +8,8 @@ interface HayLike is IERC20{ function transferFrom(address, address, uint256) external returns (bool); function approve(address, uint256) external returns (bool); + + function mint(address, uint256) external; + + function burn(address, uint256) external; } diff --git a/contracts/interfaces/IAdapter.sol b/contracts/interfaces/IAdapter.sol new file mode 100644 index 00000000..064ac852 --- /dev/null +++ b/contracts/interfaces/IAdapter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IAdapter { + function deposit(uint256 amount) external; + + function withdraw(address account, uint256 amount) external; + + function totalAvailableAmount() external returns (uint256); + + function withdrawAll() external returns (uint256); + + function netDepositAmount() external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/interfaces/IEarnPool.sol b/contracts/interfaces/IEarnPool.sol new file mode 100644 index 00000000..475f8f98 --- /dev/null +++ b/contracts/interfaces/IEarnPool.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IEarnPool { + function deposit(address account, uint256 gemAmount, uint256 lisUSDAmount) external; +} \ No newline at end of file diff --git a/contracts/interfaces/ILisUSDPool.sol b/contracts/interfaces/ILisUSDPool.sol new file mode 100644 index 00000000..39c49ca2 --- /dev/null +++ b/contracts/interfaces/ILisUSDPool.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface ILisUSDPool { + function depositFor(address pool, address account, uint256 amount) external; +} \ No newline at end of file diff --git a/contracts/interfaces/IPSM.sol b/contracts/interfaces/IPSM.sol new file mode 100644 index 00000000..936903b9 --- /dev/null +++ b/contracts/interfaces/IPSM.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IPSM { + function buy(uint256 amount) external; + + function sell(uint256 amount) external; + + function token() external view returns (address); +} \ No newline at end of file diff --git a/contracts/interfaces/IVBep20Delegate.sol b/contracts/interfaces/IVBep20Delegate.sol new file mode 100644 index 00000000..5c848b5d --- /dev/null +++ b/contracts/interfaces/IVBep20Delegate.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IVBep20Delegate { + function mint(uint256 mintAmount) external returns (uint256); + + function redeem(uint256 redeemTokens) external returns (uint256); + + function redeemUnderlying(uint256 redeemAmount) external returns (uint256); + + function balanceOfUnderlying(address owner) external returns (uint256); +} \ No newline at end of file diff --git a/contracts/interfaces/IVaultManager.sol b/contracts/interfaces/IVaultManager.sol new file mode 100644 index 00000000..50a66db1 --- /dev/null +++ b/contracts/interfaces/IVaultManager.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IVaultManager { + function deposit(uint256 amount) external; + + function withdraw(address receiver, uint256 amount) external; + + function getTotalNetDepositAmount() external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/interfaces/VatLike.sol b/contracts/interfaces/VatLike.sol index 67ffe202..82e4efc1 100644 --- a/contracts/interfaces/VatLike.sol +++ b/contracts/interfaces/VatLike.sol @@ -43,4 +43,6 @@ interface VatLike { function cage() external; function uncage() external; + + function debt() external view returns (uint256); } diff --git a/contracts/mock/MockUSDC.sol b/contracts/mock/MockUSDC.sol new file mode 100644 index 00000000..8efb3a07 --- /dev/null +++ b/contracts/mock/MockUSDC.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockUSDC is ERC20 { + constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) { + uint256 initialSupply = 1e18 * 1e9; + _mint(msg.sender, initialSupply); + } +} \ No newline at end of file diff --git a/contracts/mock/psm/MockVenus.sol b/contracts/mock/psm/MockVenus.sol new file mode 100644 index 00000000..baefe700 --- /dev/null +++ b/contracts/mock/psm/MockVenus.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract MockVenus is ERC20 { + using SafeERC20 for IERC20; + address public underlying; + + constructor(address _underlying) ERC20("MockVenus", "MockVenus") { + underlying = _underlying; + } + + function mint(uint256 amount) external returns (uint256) { + IERC20(underlying).safeTransferFrom(msg.sender, address(this), amount); + + _mint(msg.sender, amount); + return amount; + } + + function redeem(uint256 amount) external returns (uint256) { + IERC20(underlying).safeTransfer(msg.sender, amount); + + _burn(msg.sender, amount); + return amount; + } + + function redeemUnderlying(uint256 amount) external returns (uint256) { + IERC20(underlying).safeTransfer(msg.sender, amount); + + _burn(msg.sender, amount); + return amount; + } + + function balanceOfUnderlying(address owner) external view returns (uint256) { + return balanceOf(owner); + } +} \ No newline at end of file diff --git a/contracts/psm/EarnPool.sol b/contracts/psm/EarnPool.sol new file mode 100644 index 00000000..86ca5ee0 --- /dev/null +++ b/contracts/psm/EarnPool.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "../interfaces/ILisUSDPool.sol"; +import "../interfaces/IPSM.sol"; + +contract EarnPool is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + + // token => psm + mapping(address => address) public psm; + + address public lisUSDPool; // lisUSD pool address + address public lisUSD; // lisUSD address + + bytes32 public constant MANAGER = keccak256("MANAGER"); // manager role + bytes32 public constant PAUSER = keccak256("PAUSER"); // pause role + + event SetLisUSDPool(address lisUSDPool); + event SetLisUSD(address lisUSD); + event SetPSM(address token, address psm); + event RemovePSM(address token); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @dev initialize contract + * @param _admin admin address + * @param _manager manager address + * @param _lisUSDPool lisUSD pool address + * @param _lisUSD lisUSD address + */ + function initialize( + address _admin, + address _manager, + address _pauser, + address _lisUSDPool, + address _lisUSD + ) public initializer { + require(_admin != address(0), "admin cannot be zero address"); + require(_manager != address(0), "manager cannot be zero address"); + require(_pauser != address(0), "pauser cannot be zero address"); + require(_lisUSDPool != address(0), "lisUSDPool cannot be zero address"); + require(_lisUSD != address(0), "lisUSD cannot be zero address"); + __AccessControl_init(); + __Pausable_init(); + __UUPSUpgradeable_init(); + + _setupRole(DEFAULT_ADMIN_ROLE, _admin); + _setupRole(MANAGER, _manager); + _setupRole(PAUSER, _pauser); + + lisUSDPool = _lisUSDPool; + lisUSD = _lisUSD; + + emit SetLisUSDPool(_lisUSDPool); + emit SetLisUSD(_lisUSD); + } + + /** + * @dev deposit token to earn pool + * @param token token address + * @param amount token amount + */ + function deposit(address token, uint256 amount) external whenNotPaused { + require(amount > 0, "amount must be greater than zero"); + require(psm[token] != address(0), "psm not set"); + + address account = msg.sender; + // transfer token to earn pool + IERC20(token).safeTransferFrom(account, address(this), amount); + + // convert token to lisUSD by psm + IERC20(token).safeIncreaseAllowance(psm[token], amount); + uint256 before = IERC20(lisUSD).balanceOf(address(this)); + IPSM(psm[token]).sell(amount); + uint256 lisUSDAmount = IERC20(lisUSD).balanceOf(address(this)) - before; + + // deposit lisUSD to lisUSD pool + IERC20(lisUSD).safeIncreaseAllowance(lisUSDPool, lisUSDAmount); + ILisUSDPool(lisUSDPool).depositFor(token, account, lisUSDAmount); + } + + /** + * @dev pause contract + */ + function pause() external onlyRole(PAUSER) { + _pause(); + } + + /** + * @dev unpause contract + */ + function unpause() external onlyRole(MANAGER) { + _unpause(); + } + + /** + * @dev set psm + * @param _token token address + * @param _psm psm address + */ + function setPSM(address _token, address _psm) external onlyRole(MANAGER) { + require(_token != address(0), "token cannot be zero address"); + require(_psm != address(0), "psm cannot be zero address"); + require(psm[_token] == address(0), "psm already set"); + require(IPSM(_psm).token() == _token, "psm token not match"); + psm[_token] = _psm; + + emit SetPSM(_token, _psm); + } + + /** + * @dev remove psm + * @param _token token address + */ + function removePSM(address _token) external onlyRole(MANAGER) { + require(psm[_token] != address(0), "psm is not set"); + delete psm[_token]; + + emit RemovePSM(_token); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/contracts/psm/LisUSDPoolSet.sol b/contracts/psm/LisUSDPoolSet.sol new file mode 100644 index 00000000..e0485bb6 --- /dev/null +++ b/contracts/psm/LisUSDPoolSet.sol @@ -0,0 +1,439 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "../hMath.sol"; +import "../interfaces/IStakeLisUSDListaDistributor.sol"; + +contract LisUSDPoolSet is AccessControlUpgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + + struct Pool { + address asset; + address distributor; + bool active; + } + + address public lisUSD; // lisUSD address + + string public name; // pool name + string public symbol; // pool symbol + mapping(address => uint256) public balanceOf; // user's share + uint256 public totalSupply; // total shares + + // pool => user => emission weights + mapping(address => mapping(address => uint256)) public poolEmissionWeights; + // user => emission weights + mapping(address => uint256) public totalUserEmissionWeights; + // pool => pool info + mapping(address => Pool) public pools; + + uint256 public rate; // share to lisUSD rate when last update + uint256 public lastUpdate; // last rate update time + uint256 public duty; // the fixed interest rate per second + uint256 public maxDuty; // max interest rate per second + address public earnPool; // earn pool address + uint256 public maxAmount; // max assets amount + // user -> last deposit time + mapping(address => uint256) private lastDepositTime; + uint256 public withdrawDelay; // withdraw delay + + bytes32 public constant MANAGER = keccak256("MANAGER"); // manager role + bytes32 public constant PAUSER = keccak256("PAUSER"); // pause role + bytes32 public constant BOT = keccak256("BOT"); // bot role + uint256 public constant RATE_SCALE = 10 ** 27; + + event Withdraw(address account, uint256 amount); + event Deposit(address account, uint256 amount); + event SetDuty(uint256 duty); + event SetMaxDuty(uint256 maxDuty); + event RegisterPool(address pool, address asset, address distributor); + event RemovePool(address pool); + event Transfer(address indexed from, address indexed to, uint256 value); + event EmergencyWithdraw(address token, uint256 amount); + event SetLisUSD(address lisUSD); + event SetWithdrawDelay(uint256 withdrawDelay); + event SetEarnPool(address earnPool); + event SetDistributor(address pool, address distributor); + event RemoveDistributor(address pool, address distributor); + event SetMaxAmount(uint256 maxAmount); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + modifier onlyEarnPool() { + require(earnPool == msg.sender, "only earnPool can call this function"); + _; + } + + /** + * @dev initialize contract + * @param _admin admin address + * @param _manager manager address + * @param _lisUSD lisUSD address + * @param _maxDuty max rate per second + */ + function initialize( + address _admin, + address _manager, + address _pauser, + address _bot, + address _lisUSD, + uint256 _maxDuty, + uint256 _withdrawDelay + ) public initializer { + require(_admin != address(0), "admin cannot be zero address"); + require(_manager != address(0), "manager cannot be zero address"); + require(_pauser != address(0), "pauser cannot be zero address"); + require(_bot != address(0), "bot cannot be zero address"); + require(_lisUSD != address(0), "lisUSD cannot be zero address"); + require(_maxDuty > RATE_SCALE, "maxDuty cannot be zero"); + + __AccessControl_init(); + __ReentrancyGuard_init(); + __UUPSUpgradeable_init(); + __Pausable_init(); + + _setupRole(DEFAULT_ADMIN_ROLE, _admin); + _setupRole(MANAGER, _manager); + _setupRole(PAUSER, _pauser); + _setupRole(BOT, _bot); + + lisUSD = _lisUSD; + + name = "lisUSD Single Staking Pool"; + symbol = "sLisUSD"; + maxDuty = _maxDuty; + + rate = RATE_SCALE; + lastUpdate = block.timestamp; + duty = RATE_SCALE; + withdrawDelay = _withdrawDelay; + + emit SetDuty(duty); + emit SetMaxDuty(_maxDuty); + emit SetLisUSD(_lisUSD); + emit SetWithdrawDelay(_withdrawDelay); + } + + /** + * @dev withdraw lisUSD + * @param _pools pools address + * @param amount amount to withdraw + */ + function withdraw(address[] memory _pools, uint256 amount) public update nonReentrant whenNotPaused { + address account = msg.sender; + require(amount > 0, "amount cannot be zero"); + + uint256 share = convertToShares(amount); + if (share * getRate() < amount * RATE_SCALE) { + share += 1; + } + + _withdraw(account, _pools, share, amount); + } + + function withdrawAll(address[] memory _pools) public update nonReentrant whenNotPaused { + address account = msg.sender; + uint256 share = balanceOf[account]; + uint256 amount = convertToAssets(share); + require(amount > 0, "amount cannot be zero"); + _withdraw(account, _pools, share, amount); + } + + function _withdraw(address account, address[] memory _pools, uint256 share, uint256 amount) private { + require(share <= balanceOf[account], "insufficient balance"); + require(block.timestamp >= withdrawDelay + lastDepositTime[account], "withdraw delay not reached"); + require(IERC20(lisUSD).balanceOf(address(this)) >= amount, "not enough balance"); + + // update shares + balanceOf[account] -= share; + totalSupply -= share; + + uint256 costWeight; + + // update pool balance + uint256 remain = amount; + for (uint i = 0; i < _pools.length; i++) { + uint256 poolBalance = poolEmissionWeights[_pools[i]][account]; + if (poolBalance >= remain) { + costWeight += remain; + poolEmissionWeights[_pools[i]][account] -= remain; + takeSnapshot(account, _pools[i]); + break; + } else { + costWeight += poolBalance; + remain -= poolBalance; + poolEmissionWeights[_pools[i]][account] = 0; + takeSnapshot(account, _pools[i]); + } + } + uint256 totalWeights = totalUserEmissionWeights[account]; + totalUserEmissionWeights[account] -= costWeight; + require( + (amount > totalWeights && costWeight == totalWeights) || (amount <= totalWeights && costWeight == amount), + "pool balance should be deducted first" + ); + + // transfer lisUSD to account + IERC20(lisUSD).safeTransfer(account, amount); + + emit Transfer(account, address(0), share); + emit Withdraw(account, amount); + } + + /** + * @dev get user's assets + * @param account account address + */ + function assetBalanceOf(address account) public view returns (uint256) { + return convertToAssets(balanceOf[account]); + } + + /** + * @dev get total lisUSD assets + */ + function totalAssets() public view returns (uint256) { + return convertToAssets(totalSupply); + } + + /** + * @dev deposit lisUSD + * @param account account address + * @param amount amount to deposit + */ + function depositFor(address pool, address account, uint256 amount) external onlyEarnPool { + _depositFor(msg.sender, pool, account, amount); + } + + /** + * @dev deposit lisUSD + * @param amount amount to deposit + */ + function deposit(uint256 amount) external { + _depositFor(msg.sender, lisUSD, msg.sender, amount); + } + + function _depositFor( + address sender, + address pool, + address account, + uint256 amount + ) private update nonReentrant whenNotPaused { + require(amount > 0, "amount cannot be zero"); + require(totalAssets() + amount <= maxAmount, "exceed max amount"); + require(pools[pool].active, "pool not active"); + // transfer lisUSD to pool + IERC20(lisUSD).safeTransferFrom(sender, address(this), amount); + + // update shares and pool balance + uint256 share = convertToShares(amount); + balanceOf[account] += share; + totalSupply += share; + poolEmissionWeights[pool][account] += amount; + totalUserEmissionWeights[account] += amount; + lastDepositTime[account] = block.timestamp; + + takeSnapshot(account, pool); + + emit Transfer(address(0), account, share); + emit Deposit(account, amount); + } + + /** + * @dev share to asset + * @param share share + */ + function convertToAssets(uint256 share) public view returns (uint256) { + return Math.mulDiv(share, getRate(), RATE_SCALE); + } + + /** + * @dev asset to share + * @param asset balance + */ + function convertToShares(uint256 asset) public view returns (uint256) { + return Math.mulDiv(asset, RATE_SCALE, getRate()); + } + + // update reward when user do write operation + modifier update() { + rate = getRate(); + lastUpdate = block.timestamp; + + _; + } + + // get rate between current time and last update time + function getRate() public view returns (uint256) { + if (duty == 0) { + return RATE_SCALE; + } + if (lastUpdate == block.timestamp) { + return rate; + } + return hMath.rmul(hMath.rpow(duty, block.timestamp - lastUpdate, hMath.ONE), rate); + } + + /** + * @dev set duty + * @param _duty duty + */ + function setDuty(uint256 _duty) public update onlyRole(BOT) { + require(_duty <= maxDuty, "duty cannot exceed max duty"); + + duty = _duty; + emit SetDuty(_duty); + } + + /** + * @dev set max duty + * @param _maxDuty max duty + */ + function setMaxDuty(uint256 _maxDuty) external onlyRole(MANAGER) { + maxDuty = _maxDuty; + emit SetMaxDuty(_maxDuty); + } + + /** + * @dev allows admin to withdraw tokens for emergency or recover any other mistaken tokens. + * @param _token token address + * @param _amount token amount + */ + function emergencyWithdraw(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_token == address(0)) { + (bool success, ) = payable(msg.sender).call{ value: _amount }(""); + require(success, "Withdraw failed"); + } else { + IERC20(_token).safeTransfer(msg.sender, _amount); + } + + emit EmergencyWithdraw(_token, _amount); + } + /** + * @dev set earn pool + * @param _earnPool earn pool address + */ + function setEarnPool(address _earnPool) external onlyRole(MANAGER) { + require(_earnPool != address(0), "earnPool cannot be zero address"); + earnPool = _earnPool; + + emit SetEarnPool(_earnPool); + } + + /** + * @dev pause contract + */ + function pause() external onlyRole(PAUSER) { + _pause(); + } + + /** + * @dev unpause contract + */ + function unpause() external onlyRole(MANAGER) { + _unpause(); + } + + /** + * @dev take snapshot of user's LisUSD staking amount + * @param user user address + * @param pool pool address + */ + function takeSnapshot(address user, address pool) public { + address distributor = pools[pool].distributor; + // ensure the distributor address is set + if (distributor != address(0)) { + IStakeLisUSDListaDistributor(distributor).takeSnapshot(user, poolEmissionWeights[pool][user]); + } + } + + function setDistributor(address pool, address _distributor) external onlyRole(MANAGER) { + require(_distributor != address(0), "distributor cannot be zero address"); + require(pools[pool].distributor == address(0), "distributor already exists"); + + pools[pool].distributor = _distributor; + + emit SetDistributor(pool, _distributor); + } + + /** + * @dev remove distributor address + * @param pool pool address + */ + function removeDistributor(address pool) external onlyRole(MANAGER) { + address distributor = pools[pool].distributor; + pools[pool].distributor = address(0); + + emit RemoveDistributor(pool, distributor); + } + + /** + * @dev register pool + * @param pool pool address + * @param asset asset address + * @param distributor distributor address + */ + function registerPool(address pool, address asset, address distributor) public onlyRole(MANAGER) { + require(pool != address(0), "pool cannot be zero address"); + require(asset != address(0), "asset cannot be zero address"); + require(!pools[pool].active, "pool already exists"); + + pools[pool] = Pool({ asset: asset, distributor: distributor, active: true }); + + emit RegisterPool(pool, asset, distributor); + if (distributor != address(0)) { + emit SetDistributor(pool, distributor); + } + } + + /** + * @dev remove pool + * @param pool pool address + */ + function removePool(address pool) external onlyRole(MANAGER) { + require(pools[pool].active, "pool not exists"); + + pools[pool].active = false; + + emit RemovePool(pool); + address distributor = pools[pool].distributor; + if (distributor != address(0)) { + pools[pool].distributor = address(0); + emit RemoveDistributor(pool, distributor); + } + } + + /** + * @dev set max amount + * @param _maxAmount max amount + */ + function setMaxAmount(uint256 _maxAmount) external onlyRole(MANAGER) { + maxAmount = _maxAmount; + + emit SetMaxAmount(_maxAmount); + } + + function decimals() public pure returns (uint8) { + return 18; + } + + /** + * @dev set withdraw delay + * @param _withdrawDelay withdraw delay + */ + function setWithdrawDelay(uint256 _withdrawDelay) external onlyRole(MANAGER) { + withdrawDelay = _withdrawDelay; + + emit SetWithdrawDelay(_withdrawDelay); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/contracts/psm/PSM.sol b/contracts/psm/PSM.sol new file mode 100644 index 00000000..9ccbfd8b --- /dev/null +++ b/contracts/psm/PSM.sol @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "../interfaces/IVaultManager.sol"; + +contract PSM is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + + address public vaultManager; // VaultManager address + address public token; // token address + uint256 public sellFee; // sell fee rate + uint256 public buyFee; // buy fee rate + address public feeReceiver; // fee receiver address + address public lisUSD; // lisUSD address + + uint256 public dailyLimit; // daily buy limit + uint256 public minSell; // min sell amount + uint256 public minBuy; // min buy amount + + uint256 public lastBuyDay; // last buy day + uint256 public dayBuyUsed; // day buy used + + uint256 public fees; // total fee + + uint256 public constant FEE_PRECISION = 10000; + + bytes32 public constant MANAGER = keccak256("MANAGER"); // manager role + bytes32 public constant PAUSER = keccak256("PAUSER"); // pause role + + event SetBuyFee(uint256 buyFee); + event SetSellFee(uint256 sellFee); + event SetFeeReceiver(address feeReceiver); + event BuyToken(address account, uint256 realAmount, uint256 fee); + event SellToken(address account, uint256 realAmount, uint256 fee); + event SetDailyLimit(uint256 dailyLimit); + event SetMinSell(uint256 minSell); + event SetMinBuy(uint256 minBuy); + event SetVaultManager(address vaultManager); + event EmergencyWithdraw(address token, uint256 amount); + event SetToken(address token); + event SetLisUSD(address lisUSD); + event Harvest(uint256 fees); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @dev initialize contract + * @param _admin admin address + * @param _manager manager address + * @param _token token address + * @param _feeReceiver fee receiver address + * @param _lisUSD lisUSD address + * @param _sellFee sell fee + * @param _buyFee buy fee + * @param _minSell min sell amount + * @param _minBuy min buy amount + */ + function initialize( + address _admin, + address _manager, + address _pauser, + address _token, + address _feeReceiver, + address _lisUSD, + uint256 _sellFee, + uint256 _buyFee, + uint256 _dailyLimit, + uint256 _minSell, + uint256 _minBuy + ) public initializer { + require(_admin != address(0), "admin cannot be zero address"); + require(_manager != address(0), "manager cannot be zero address"); + require(_pauser != address(0), "pauser cannot be zero address"); + require(_token != address(0), "token cannot be zero address"); + require(_feeReceiver != address(0), "feeReceiver cannot be zero address"); + require(_lisUSD != address(0), "lisUSD cannot be zero address"); + require(_sellFee <= FEE_PRECISION, "sellFee must be less or equal than FEE_PRECISION"); + require(_buyFee <= FEE_PRECISION, "buyFee must be less or equal than FEE_PRECISION"); + require(_dailyLimit >= _minBuy, "dailyLimit must be greater or equal than minBuy"); + + __AccessControl_init(); + __Pausable_init(); + __UUPSUpgradeable_init(); + + _setupRole(DEFAULT_ADMIN_ROLE, _admin); + _setupRole(MANAGER, _manager); + _setupRole(PAUSER, _pauser); + + token = _token; + sellFee = _sellFee; + buyFee = _buyFee; + feeReceiver = _feeReceiver; + lisUSD = _lisUSD; + + dailyLimit = _dailyLimit; + minSell = _minSell; + minBuy = _minBuy; + + emit SetBuyFee(_buyFee); + emit SetSellFee(_sellFee); + emit SetFeeReceiver(_feeReceiver); + emit SetDailyLimit(_dailyLimit); + emit SetMinSell(_minSell); + emit SetMinBuy(_minBuy); + emit SetToken(_token); + emit SetLisUSD(_lisUSD); + } + + /** + * @dev sell token to get lisUSD + * @param amount token amount + */ + function sell(uint256 amount) external whenNotPaused { + require(amount >= minSell, "amount smaller than minSell"); + // calculate fee and real amount + uint256 fee = Math.mulDiv(amount, sellFee, FEE_PRECISION); + uint256 realAmount = amount - fee; + + // check sell limit + require(amount <= getTotalSellLimit(), "exceed sell limit"); + + // transfer token from user + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + // transfer lisUSD to user + IERC20(lisUSD).safeTransfer(msg.sender, realAmount); + + // deposit token to vault manager + IERC20(token).safeIncreaseAllowance(vaultManager, amount); + IVaultManager(vaultManager).deposit(amount); + + // transfer fee to fee receiver + if (fee > 0) { + fees += fee; + } + emit SellToken(msg.sender, realAmount, fee); + } + + /** + * @dev buy token with lisUSD + * @param amount lisUSD amount + */ + function buy(uint256 amount) external whenNotPaused { + // check buy limit + checkAndUpdateBuyUsed(amount); + + // calculate fee and real amount + uint256 fee = Math.mulDiv(amount, buyFee, FEE_PRECISION); + uint256 realAmount = amount - fee; + + // transfer lisUSD from user and withdraw token from vault manager + if (realAmount > 0) { + IERC20(lisUSD).safeTransferFrom(msg.sender, address(this), amount); + IVaultManager(vaultManager).withdraw(msg.sender, realAmount); + } + + // transfer fee to fee receiver + if (fee > 0) { + fees += fee; + } + emit BuyToken(msg.sender, realAmount, fee); + } + + // check buy limit + function checkBuyLimit(uint256 amount) public view returns (bool) { + // check min buy amount + if (amount < minBuy) { + return false; + } + // check daily buy limit + if (getDay() == lastBuyDay && dayBuyUsed + amount > dailyLimit) { + return false; + } + if (getDay() != lastBuyDay && amount > dailyLimit) { + return false; + } + return true; + } + + // check and update buy used + function checkAndUpdateBuyUsed(uint256 amount) private { + require(checkBuyLimit(amount), "amount smaller than minBuy or exceed buy limit"); + + // update total sell and buy used + if (getDay() != lastBuyDay) { + lastBuyDay = getDay(); + dayBuyUsed = 0; + } + + dayBuyUsed += amount; + } + + // get day + function getDay() public view returns (uint256) { + return block.timestamp / 1 days; + } + + /** + * @dev set vault manager address + * @param _vaultManager vault manager address + */ + function setVaultManager(address _vaultManager) external onlyRole(MANAGER) { + require(_vaultManager != address(0), "VaultManager cannot be zero address"); + require(_vaultManager != vaultManager, "VaultManager already set"); + vaultManager = _vaultManager; + emit SetVaultManager(_vaultManager); + } + + /** + * @dev set buy fee + * @param _buyFee buy fee + */ + function setBuyFee(uint256 _buyFee) external onlyRole(MANAGER) { + require(_buyFee <= FEE_PRECISION, "buyFee must be less or equal than FEE_PRECISION"); + buyFee = _buyFee; + emit SetBuyFee(_buyFee); + } + + /** + * @dev set sell fee + * @param _sellFee sell fee + */ + function setSellFee(uint256 _sellFee) external onlyRole(MANAGER) { + require(_sellFee <= FEE_PRECISION, "sellFee must be less or equal than FEE_PRECISION"); + sellFee = _sellFee; + emit SetSellFee(_sellFee); + } + + /** + * @dev set fee receiver address + * @param _feeReceiver fee receiver address + */ + function setFeeReceiver(address _feeReceiver) external onlyRole(MANAGER) { + require(_feeReceiver != address(0), "feeReceiver cannot be zero address"); + feeReceiver = _feeReceiver; + emit SetFeeReceiver(_feeReceiver); + } + + /** + * @dev set daily buy limit + * @param _dailyLimit daily limit + */ + function setDailyLimit(uint256 _dailyLimit) external onlyRole(MANAGER) { + require(_dailyLimit >= minBuy, "dailyLimit must be greater or equal than minBuy"); + + dailyLimit = _dailyLimit; + emit SetDailyLimit(_dailyLimit); + } + + /** + * @dev set min sell amount + * @param _minSell min sell amount + */ + function setMinSell(uint256 _minSell) external onlyRole(MANAGER) { + minSell = _minSell; + emit SetMinSell(_minSell); + } + + /** + * @dev set min buy amount + * @param _minBuy min buy amount + */ + function setMinBuy(uint256 _minBuy) external onlyRole(MANAGER) { + require(dailyLimit >= _minBuy, "minBuy must be less or equal than dailyLimit"); + + minBuy = _minBuy; + emit SetMinBuy(_minBuy); + } + + /** + * @dev get total buy limit + * @return total buy limit + */ + function getTotalBuyLimit() external view returns (uint256) { + return IVaultManager(vaultManager).getTotalNetDepositAmount(); + } + + /** + * @dev get total sell limit + * @return total sell limit + */ + function getTotalSellLimit() public view returns (uint256) { + return IERC20(lisUSD).balanceOf(address(this)) - fees; + } + + /** + * @dev get day buy left + * @return day buy left + */ + function getDayBuyLeft() external view returns (uint256) { + if (getDay() == lastBuyDay) { + return dailyLimit - dayBuyUsed; + } + return dailyLimit; + } + + /** + * @dev harvest fees + */ + function harvest() external { + if (fees > 0) { + uint256 _fees = fees; + fees = 0; + IERC20(lisUSD).safeTransfer(feeReceiver, _fees); + + emit Harvest(_fees); + } + } + + /** + * @dev pause contract + */ + function pause() external onlyRole(PAUSER) { + _pause(); + } + + /** + * @dev unpause contract + */ + function unpause() external onlyRole(MANAGER) { + _unpause(); + } + + /** + * @dev allows admin to withdraw tokens for emergency or recover any other mistaken tokens. + * @param _token token address + * @param _amount token amount + */ + function emergencyWithdraw(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_token == address(0)) { + (bool success, ) = payable(msg.sender).call{ value: _amount }(""); + require(success, "Withdraw failed"); + } else { + IERC20(_token).safeTransfer(msg.sender, _amount); + } + emit EmergencyWithdraw(_token, _amount); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/contracts/psm/VaultManager.sol b/contracts/psm/VaultManager.sol new file mode 100644 index 00000000..8541e4f1 --- /dev/null +++ b/contracts/psm/VaultManager.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "../interfaces/IAdapter.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +contract VaultManager is ReentrancyGuardUpgradeable, AccessControlUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + address public psm; // PSM address + address public token; // token address + + struct Adapter { + address adapter; // adapter address + bool active; // active status + uint256 point; // adapter point + } + + Adapter[] public adapters; // adapter list + + bytes32 public constant MANAGER = keccak256("MANAGER"); // manager role + bytes32 public constant BOT = keccak256("BOT"); // bot role + + event SetPSM(address psm); + event SetToken(address token); + event SetAdapter(address adapter, bool active, uint256 point); + event AddAdapter(address adapter, uint256 point); + event Deposit(uint256 amount); + event Withdraw(address receiver, uint256 amount); + event ReBalance(uint256 amount); + event EmergencyWithdraw(address account, uint256 amount); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @dev initialize contract + * @param _admin admin address + * @param _manager manager address + * @param _psm PSM address + * @param _token token address + */ + function initialize(address _admin, address _manager, address _psm, address _token) public initializer { + require(_admin != address(0), "admin cannot be zero address"); + require(_manager != address(0), "manager cannot be zero address"); + require(_psm != address(0), "psm cannot be zero address"); + require(_token != address(0), "token cannot be zero address"); + __AccessControl_init(); + __UUPSUpgradeable_init(); + __ReentrancyGuard_init(); + + _setupRole(DEFAULT_ADMIN_ROLE, _admin); + _setupRole(MANAGER, _manager); + + psm = _psm; + token = _token; + + emit SetPSM(_psm); + emit SetToken(_token); + } + + modifier onlyPSM() { + require(msg.sender == psm, "Only PSM can call this function"); + _; + } + + modifier onlyPSMOrManager() { + require(msg.sender == psm || hasRole(MANAGER, msg.sender), "Only PSM or Manager can call this function"); + _; + } + + /** + * @dev deposit token to adapters, only PSM or manager can call this function + * @param amount deposit amount + */ + function deposit(uint256 amount) external nonReentrant onlyPSMOrManager { + require(amount > 0, "deposit amount cannot be zero"); + + // transfer token to this contract + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + _distribute(amount); + + emit Deposit(amount); + } + + function _distribute(uint256 amount) private { + uint256 remain = amount; + uint256 totalPoint = getTotalPoint(); + + if (totalPoint == 0) { + return; + } + // deposit token to adapters by adapter point + for (uint256 i = 0; i < adapters.length; i++) { + if (adapters[i].active && adapters[i].point > 0) { + // only active adapter can be used + // adapterAmount = depositAmount * point / totalPoint + uint256 amt = Math.mulDiv(amount, adapters[i].point, totalPoint); + if (amt > 0) { + IERC20(token).safeIncreaseAllowance(adapters[i].adapter, amt); + IAdapter(adapters[i].adapter).deposit(amt); + remain -= amt; + } + } + } + } + + /** + * @dev withdraw token from adapters, only PSM or manager can call this function + * @param receiver receiver address + * @param amount withdraw amount + */ + function withdraw(address receiver, uint256 amount) external nonReentrant onlyPSMOrManager { + require(amount > 0, "withdraw amount cannot be zero"); + + uint256 remain = amount; + uint256 vaultBalance = IERC20(token).balanceOf(address(this)); + if (vaultBalance >= amount) { + // withdraw token from vault manager + IERC20(token).safeTransfer(receiver, amount); + remain = 0; + } else { + if (vaultBalance > 0) { + IERC20(token).safeTransfer(receiver, vaultBalance); + remain -= vaultBalance; + } + } + + if (remain > 0) { + require(adapters.length > 0, "no adapter"); + // withdraw token from adapters + uint256 startIdx = block.number % adapters.length; + + for (uint256 i = 0; i < adapters.length; i++) { + uint256 idx = (startIdx + i) % adapters.length; + // only active adapter can be used + if (adapters[idx].active) { + uint256 netDeposit = IAdapter(adapters[idx].adapter).netDepositAmount(); + if (netDeposit == 0) { + continue; + } + if (netDeposit >= remain) { + IAdapter(adapters[idx].adapter).withdraw(receiver, remain); + remain = 0; + break; + } else { + remain -= netDeposit; + IAdapter(adapters[idx].adapter).withdraw(receiver, netDeposit); + } + } + } + } + + require(remain == 0, "not enough available balance"); + + emit Withdraw(receiver, amount); + } + + /** + * @dev add adapter + * @param adapter adapter address + * @param point adapter point + */ + function addAdapter(address adapter, uint256 point) external onlyRole(MANAGER) { + require(adapter != address(0), "adapter cannot be zero address"); + require(point > 0, "point cannot be zero"); + for (uint256 i = 0; i < adapters.length; i++) { + require(adapters[i].adapter != adapter, "adapter already exists"); + } + + adapters.push(Adapter({ adapter: adapter, active: true, point: point })); + + emit AddAdapter(adapter, point); + } + + /** + * @dev update adapter + * @param index adapter index + * @param active active status + * @param point adapter point + */ + function setAdapter(uint256 index, bool active, uint256 point) external onlyRole(MANAGER) { + require(index < adapters.length, "index out of range"); + adapters[index].active = active; + adapters[index].point = point; + + emit SetAdapter(adapters[index].adapter, active, point); + } + + /** + * @dev get total net deposit amount + */ + function getTotalNetDepositAmount() public view returns (uint256) { + uint256 amount = IERC20(token).balanceOf(address(this)); + for (uint256 i = 0; i < adapters.length; i++) { + amount += IAdapter(adapters[i].adapter).netDepositAmount(); + } + return amount; + } + + /** + * @dev get total point + */ + function getTotalPoint() public view returns (uint256) { + uint256 totalPoint; + for (uint256 i = 0; i < adapters.length; i++) { + if (adapters[i].active) { + totalPoint += adapters[i].point; + } + } + + return totalPoint; + } + + /** + * @dev rebalance token to adapters, only bot can call this function + */ + function rebalance() external onlyRole(BOT) { + require(adapters.length > 0, "no adapter"); + + for (uint256 i = 0; i < adapters.length; i++) { + if (adapters[i].active) { + IAdapter(adapters[i].adapter).withdrawAll(); + } + } + uint256 amount = IERC20(token).balanceOf(address(this)); + + if (amount > 0) { + _distribute(amount); + } + + emit ReBalance(amount); + } + + /** + * @dev emergency withdraw token from adapters + * @param index adapter index + */ + function emergencyWithdraw(uint256 index) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(index < adapters.length, "index out of range"); + uint256 amount = IAdapter(adapters[index].adapter).withdrawAll() + IERC20(token).balanceOf(address(this)); + + IERC20(token).safeTransfer(msg.sender, amount); + + emit EmergencyWithdraw(msg.sender, amount); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/contracts/psm/VenusAdapter.sol b/contracts/psm/VenusAdapter.sol new file mode 100644 index 00000000..2733be49 --- /dev/null +++ b/contracts/psm/VenusAdapter.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "../interfaces/IVBep20Delegate.sol"; + +contract VenusAdapter is AccessControlUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + address public vaultManager; // vault manager address + address public token; // token address + address public vToken; // vToken address + uint256 public netDepositAmount; // user net deposit amount + address public feeReceiver; // fee receiver address + + bytes32 public constant MANAGER = keccak256("MANAGER"); // manager role + + event Deposit(uint256 amount); + event Withdraw(address account, uint256 amount); + event Harvest(address account, uint256 amount); + event SetFeeReceiver(address feeReceiver); + event SetVaultManager(address vaultManager); + event SetVToken(address vToken); + event SetToken(address token); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + modifier onlyVaultManager() { + require(msg.sender == vaultManager, "only VaultManager can call this function"); + _; + } + + /** + * @dev initialize contract + * @param _admin admin address + * @param _manager manager address + * @param _vaultManager vault manager address + * @param _token token address + * @param _vToken vToken address + * @param _feeReceiver fee receiver address + */ + function initialize( + address _admin, + address _manager, + address _vaultManager, + address _token, + address _vToken, + address _feeReceiver + ) public initializer { + require(_admin != address(0), "admin cannot be zero address"); + require(_manager != address(0), "manager cannot be zero address"); + require(_vaultManager != address(0), "vaultManager cannot be zero address"); + require(_token != address(0), "token cannot be zero address"); + require(_vToken != address(0), "vToken cannot be zero address"); + require(_feeReceiver != address(0), "feeReceiver cannot be zero address"); + + __AccessControl_init(); + __UUPSUpgradeable_init(); + + _setupRole(DEFAULT_ADMIN_ROLE, _admin); + _setupRole(MANAGER, _manager); + + vaultManager = _vaultManager; + token = _token; + vToken = _vToken; + feeReceiver = _feeReceiver; + + emit SetVaultManager(_vaultManager); + emit SetToken(_token); + emit SetVToken(_vToken); + emit SetFeeReceiver(_feeReceiver); + } + + /** + * @dev deposit token by vault manager + * @param amount deposit amount + */ + function deposit(uint256 amount) external onlyVaultManager { + require(amount > 0, "deposit amount cannot be zero"); + IERC20(token).safeTransferFrom(vaultManager, address(this), amount); + IERC20(token).safeIncreaseAllowance(vToken, amount); + + netDepositAmount += amount; + + // deposit to venus pool + IVBep20Delegate(vToken).mint(amount); + + emit Deposit(amount); + } + + /** + * @dev withdraw token by vault manager + * @param account withdraw account + * @param amount withdraw amount + */ + function withdraw(address account, uint256 amount) external onlyVaultManager { + require(amount > 0, "withdraw amount cannot be zero"); + require(amount <= netDepositAmount, "withdraw amount exceeds net deposit"); + + netDepositAmount -= amount; + + IVBep20Delegate(vToken).redeemUnderlying(amount); + + // transfer token to account + IERC20(token).safeTransfer(account, amount); + + emit Withdraw(account, amount); + } + + /** + * @dev withdraw all token to vault manager + */ + function withdrawAll() external onlyVaultManager returns (uint256) { + // harvest interest to fee receiver + harvest(); + + // withdraw all token to vault manager + netDepositAmount = 0; + + uint256 totalAmount; + uint256 vTokenAmount = IERC20(vToken).balanceOf(address(this)); + + if (vTokenAmount > 0) { + totalAmount = _withdrawFromVenus(vTokenAmount); + } + if (totalAmount > 0) { + IERC20(token).safeTransfer(vaultManager, totalAmount); + emit Withdraw(vaultManager, totalAmount); + } + return totalAmount; + } + + /** + * @dev harvest interest to fee receiver + */ + function harvest() public { + uint256 totalAmount = IVBep20Delegate(vToken).balanceOfUnderlying(address(this)); + if (totalAmount > netDepositAmount) { + // calculate interest and redeem amount + uint256 interest = totalAmount - netDepositAmount; + IVBep20Delegate(vToken).redeemUnderlying(interest); + IERC20(token).safeTransfer(feeReceiver, interest); + + emit Harvest(feeReceiver, interest); + } + } + + function _withdrawFromVenus(uint256 vTokenAmount) private returns (uint256) { + uint256 before = IERC20(token).balanceOf(address(this)); + IERC20(vToken).safeIncreaseAllowance(vToken, vTokenAmount); + IVBep20Delegate(vToken).redeem(vTokenAmount); + return IERC20(token).balanceOf(address(this)) - before; + } + + /** + * @dev set fee receiver + * @param _feeReceiver fee receiver address + */ + function setFeeReceiver(address _feeReceiver) external onlyRole(MANAGER) { + require(_feeReceiver != address(0), "feeReceiver cannot be zero address"); + feeReceiver = _feeReceiver; + emit SetFeeReceiver(_feeReceiver); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/diagrams/PSM.drawio.png b/diagrams/PSM.drawio.png new file mode 100644 index 00000000..086719f5 Binary files /dev/null and b/diagrams/PSM.drawio.png differ diff --git a/hardhat.config.ts b/hardhat.config.ts index 7aba4f0a..644f06e8 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -14,6 +14,7 @@ import * as fs from "fs"; import "@typechain/hardhat"; import "@nomicfoundation/hardhat-ethers"; import "@nomicfoundation/hardhat-chai-matchers"; +import "@nomicfoundation/hardhat-foundry"; const config: HardhatUserConfig = { solidity: { diff --git a/lib/forge-std b/lib/forge-std index 1714bee7..8f24d6b0 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/package.json b/package.json index 04d30919..e63f2a14 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "license": "ISC", "dependencies": { "@chainlink/contracts": "^0.4.1", + "@nomicfoundation/hardhat-foundry": "^1.1.2", "@nomiclabs/hardhat-ethers": "^2.0.5", "@nomiclabs/hardhat-etherscan": "^3.0.3", "@nomiclabs/hardhat-truffle5": "^2.0.5", @@ -20,7 +21,8 @@ "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/contracts": "4.8.3", "@openzeppelin/contracts-upgradeable": "4.8.3", - "@pythnetwork/pyth-sdk-solidity": "^3.1.0" + "@pythnetwork/pyth-sdk-solidity": "^3.1.0", + "bluebird": "^3.7.2" }, "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", diff --git a/scripts/dev/psm/deploy_earnPool.js b/scripts/dev/psm/deploy_earnPool.js new file mode 100644 index 00000000..c0ed035f --- /dev/null +++ b/scripts/dev/psm/deploy_earnPool.js @@ -0,0 +1,69 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') +const Promise = require('bluebird'); + +let lisUSDPool = '0x371588eBFA6D6fA9E38637D9880CC3327b33f82F'; +let lisUSD = '0x785b5d1Bde70bD6042877cA08E4c73e0a40071af'; + +const psms = [{ + psm: '0x89F5e21Ed5d716FcD86dfF00fDAbf9Bbc9327AC5', + coin: '0xA528b0E61b72A0191515944cD8818a88d1D1D22b', + name: 'USDC', + distributor: '0x0000000000000000000000000000000000000000', +}, { + psm: '0xF915BD8Db101ABA1253a17B2e359B4B9C0d50F84', + coin: '0x49b1401B4406Fe0B32481613bF1bC9Fe4B9378aC', + name: 'USDT', + distributor: '0x0000000000000000000000000000000000000000', +}, { + psm: '0x7616c413F29059D5002B0cCdFc2c82526EdA3E23', + coin: '0xadbccCa89eC498F8B9B7F6A4B05206b113676861', + name: 'FDUSD', + distributor: '0x0000000000000000000000000000000000000000', +}] + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + + const EarnPool = await hre.ethers.getContractFactory('EarnPool'); + const earnPoll = await upgrades.deployProxy(EarnPool, [ + deployer, + deployer, + lisUSDPool, + lisUSD, + ]); + await earnPoll.waitForDeployment(); + + const proxyAddress = await earnPoll.getAddress(); + + console.log('EarnPool deployed to:', proxyAddress); + try { + await run("verify:verify", { + address: proxyAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + + const lisUSDPoolContract = await ethers.getContractAt('LisUSDPoolSet', lisUSDPool); + const earnPoolContract = await ethers.getContractAt('EarnPool', proxyAddress); + + await lisUSDPoolContract.setEarnPool(proxyAddress); + await Promise.delay(3000); + for (let i = 0; i < psms.length; i++) { + const psm = psms[i]; + await earnPoolContract.setPSM(psm.coin, psm.psm); + await Promise.delay(3000); + await lisUSDPoolContract.registerPool(psm.coin, psm.coin, psm.distributor); + await Promise.delay(3000); + } + console.log("EarnPool deploy and setup done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/dev/psm/deploy_lisUSDPool.js b/scripts/dev/psm/deploy_lisUSDPool.js new file mode 100644 index 00000000..b677ac5d --- /dev/null +++ b/scripts/dev/psm/deploy_lisUSDPool.js @@ -0,0 +1,46 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let lisUSD = '0x785b5d1Bde70bD6042877cA08E4c73e0a40071af'; +let maxDuty = '1000000034836767751273470154'; // 200% +let zero = "0x0000000000000000000000000000000000000000"; +let maxAmount = "10000000000000000000000000"; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + + const LisUSDPool = await hre.ethers.getContractFactory('LisUSDPoolSet'); + const lisUSDPool = await upgrades.deployProxy(LisUSDPool, [ + deployer, + deployer, + lisUSD, + maxDuty + ]); + await lisUSDPool.waitForDeployment(); + + const proxyAddress = await lisUSDPool.getAddress(); + + console.log('LisUSDPool deployed to:', proxyAddress); + try { + await run("verify:verify", { + address: proxyAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + + const LisUSDPoolContract = await ethers.getContractAt('LisUSDPoolSet', proxyAddress); + + await LisUSDPoolContract.registerPool(lisUSD, lisUSD, zero); + await LisUSDPoolContract.setMaxAmount(maxAmount); + + console.log('LisUSDPoolSet deploy and setup done'); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/dev/psm/deploy_mockUSDC.js b/scripts/dev/psm/deploy_mockUSDC.js new file mode 100644 index 00000000..b76d5fdb --- /dev/null +++ b/scripts/dev/psm/deploy_mockUSDC.js @@ -0,0 +1,24 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +async function main() { + const MockUSDCFactory = await hre.ethers.getContractFactory('MockUSDC'); + const mockUSDC = await MockUSDCFactory.deploy('USDC', 'USDC'); + await mockUSDC.deploymentTransaction().wait(6); + const address = await mockUSDC.getAddress(); + + console.log('MockUSDC deployed to:', address); + await run("verify:verify", { + address, + constructorArguments: ['USDC', 'USDC'], + contract: 'contracts/mock/MockUSDC.sol:MockUSDC' + }); + +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/dev/psm/deploy_psm.js b/scripts/dev/psm/deploy_psm.js new file mode 100644 index 00000000..3d620e6d --- /dev/null +++ b/scripts/dev/psm/deploy_psm.js @@ -0,0 +1,49 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let usdc = '0xadbccCa89eC498F8B9B7F6A4B05206b113676861'; +let lisUSD = '0x785b5d1Bde70bD6042877cA08E4c73e0a40071af'; +let sellFee = 0; +let buyFee = 500; +let dailyLimit = '10000000000000000000000000' // 1e25; +let minSell = '1000000000000000000'; // 1e18; +let minBuy = '1000000000000000000'; // 1e18; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + + const PSM = await hre.ethers.getContractFactory('PSM'); + const psm = await upgrades.deployProxy(PSM, [ + deployer, + deployer, + usdc, + deployer, + lisUSD, + sellFee, + buyFee, + dailyLimit, + minSell, + minBuy + ]); + await psm.waitForDeployment(); + + const proxyAddress = await psm.getAddress(); + + console.log('PSM deployed to:', proxyAddress); + try { + await run("verify:verify", { + address: proxyAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/dev/psm/deploy_psm_all.js b/scripts/dev/psm/deploy_psm_all.js new file mode 100644 index 00000000..c6445968 --- /dev/null +++ b/scripts/dev/psm/deploy_psm_all.js @@ -0,0 +1,244 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') +const string_decoder = require("node:string_decoder"); +const Promise = require("bluebird"); +const {verifyImpContract} = require("../../upgrades/utils/upgrade_utils"); +const {atan2} = require("math.js/lib/trigonometric"); + +let usdc = '0xA528b0E61b72A0191515944cD8818a88d1D1D22b'; +let usdt = '0x49b1401B4406Fe0B32481613bF1bC9Fe4B9378aC'; +let fdusd = '0xadbccCa89eC498F8B9B7F6A4B05206b113676861'; +let lisUSD = '0x785b5d1Bde70bD6042877cA08E4c73e0a40071af'; +let sellFee = 0; +let buyFee = 500; +let dailyLimit = '10000000000000000000000000' // 1e25; +let minSell = '1000000000000000000'; // 1e18; +let minBuy = '1000000000000000000'; // 1e18; +let psms = {} +let maxDuty = '1000000034836767751273470154'; // 200% +let withdrawDelay = 5; +let maxAmount = "10000000000000000000000000"; +let duty = '1000000003022265980097390211'; // 10% + +const distributors = { + 'USDC': '0x9d9cfDc14D22a4eC4a31D6AfeD892Ac07913705d', + 'USDT': '0x08853f4Ae95a4a163c7Ecfb5aa251681c5FcDcB7', + 'FDUSD': '0xdB38311d06ff3B1764BF51bFb5B9Dbb6297e116a', + 'lisUSD': '0x0000000000000000000000000000000000000000', +} + +async function main() { + await deployPSMAll(usdc, 'USDC'); + await deployPSMAll(usdt, 'USDT'); + await deployPSMAll(fdusd, 'FDUSD'); + + await deployPools(); +} + +async function verifyMockVenus(address, token) { + await run("verify:verify", { + address: address, + constructorArguments: [token], + contract: 'contracts/mock/psm/MockVenus.sol:MockVenus' + }); +} + +async function deployPSMAll(token, name) { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + const admin = deployer; + const manager = deployer; + const pauser = deployer; + const feeReceiver = deployer; + console.log(`---------------------- ${name} ---------------------- `); + // deploy PSM USDC + const PSM = await hre.ethers.getContractFactory('PSM'); + const psm = await upgrades.deployProxy(PSM, [ + admin, + manager, + pauser, + token, + feeReceiver, + lisUSD, + sellFee, + buyFee, + dailyLimit, + minSell, + minBuy + ]); + await psm.waitForDeployment(); + + const psmAddress = await psm.getAddress(); + + try { + await run("verify:verify", { + address: psmAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + + psms[name] = psmAddress; + console.log(`PSM ${name} deployed to:`, psmAddress); + + const VaultManager = await hre.ethers.getContractFactory('VaultManager'); + const vaultManager = await upgrades.deployProxy(VaultManager, [ + admin, + manager, + psmAddress, + token + ]); + await vaultManager.waitForDeployment(); + + const vaultManagerAddress = await vaultManager.getAddress(); + + try { + await run("verify:verify", { + address: vaultManagerAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + + const psmContract = await ethers.getContractAt('PSM', psmAddress); + + await psmContract.setVaultManager(vaultManagerAddress); + + console.log(`VaultManager ${name} deployed to:`, vaultManagerAddress); + + const MokcVenus = await hre.ethers.getContractFactory('MockVenus'); + const mockVenus = await MokcVenus.deploy(token); + await mockVenus.deploymentTransaction().wait(6); + const mockVenusAddress = await mockVenus.getAddress(); + + try { + await run("verify:verify", { + address: mockVenusAddress, + constructorArguments: [token], + contract: 'contracts/mock/psm/MockVenus.sol:MockVenus' + }); + } catch (error) { + console.error('error verifying contract:', error); + } + console.log(`MockVenus ${name} deployed to:`, mockVenusAddress); + + const VenusAdapter = await hre.ethers.getContractFactory('VenusAdapter'); + const venusAdapter = await upgrades.deployProxy(VenusAdapter, [ + admin, + manager, + vaultManagerAddress, + mockVenusAddress, + token, + mockVenusAddress, + feeReceiver + ]); + + await venusAdapter.waitForDeployment(); + + const venusAdapterAddress = await venusAdapter.getAddress(); + + try { + await run("verify:verify", { + address: venusAdapterAddress, + constructorArguments: [token], + contract: 'contracts/mock/psm/MockVenus.sol:MockVenus' + }); + } catch (error) { + console.error('error verifying contract:', error); + } + console.log(`VenusAdapter ${name} deployed to:`, venusAdapterAddress); + + const vaultManagerContract = await ethers.getContractAt('VaultManager', vaultManagerAddress); + + await vaultManagerContract.addAdapter(venusAdapterAddress, 100); + + console.log('VenusAdapter deploy and setup done') +} + +async function deployPools() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + const admin = deployer; + const manager = deployer; + const pauser = deployer; + const LisUSDPool = await hre.ethers.getContractFactory('LisUSDPoolSet'); + const lisUSDPool = await upgrades.deployProxy(LisUSDPool, [ + admin, + manager, + pauser, + lisUSD, + maxDuty, + withdrawDelay + ]); + await lisUSDPool.waitForDeployment(); + + const lisUSDPoolAddress = await lisUSDPool.getAddress(); + + try { + await run("verify:verify", { + address: lisUSDPoolAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + + const LisUSDPoolContract = await ethers.getContractAt('LisUSDPoolSet', lisUSDPoolAddress); + + await LisUSDPoolContract.setMaxAmount(maxAmount); + + console.log('LisUSDPool deployed to:', lisUSDPoolAddress); + + const EarnPool = await hre.ethers.getContractFactory('EarnPool'); + const earnPoll = await upgrades.deployProxy(EarnPool, [ + admin, + manager, + pauser, + lisUSDPoolAddress, + lisUSD, + ]); + await earnPoll.waitForDeployment(); + + const earnPollAddress = await earnPoll.getAddress(); + + try { + await run("verify:verify", { + address: earnPollAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + console.log('EarnPool deployed to:', earnPollAddress); + + const earnPoolContract = await ethers.getContractAt('EarnPool', earnPollAddress); + + await LisUSDPoolContract.setEarnPool(earnPollAddress); + await Promise.delay(3000); + + earnPoolContract.setPSM(usdc, psms['USDC']); + await Promise.delay(3000); + earnPoolContract.setPSM(usdt, psms['USDT']); + await Promise.delay(3000); + earnPoolContract.setPSM(fdusd, psms['FDUSD']); + await Promise.delay(3000); + + LisUSDPoolContract.registerPool(usdc, usdc, distributors['USDC']); + await Promise.delay(3000); + LisUSDPoolContract.registerPool(usdt, usdt, distributors['USDT']); + await Promise.delay(3000); + LisUSDPoolContract.registerPool(fdusd, fdusd, distributors['FDUSD']); + await Promise.delay(3000); + LisUSDPoolContract.registerPool(lisUSD, lisUSD, distributors['lisUSD']); + await Promise.delay(3000); + + //setDuty + LisUSDPoolContract.setDuty(duty); + + console.log("EarnPool deploy and setup done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/dev/psm/deploy_vaultManager.js b/scripts/dev/psm/deploy_vaultManager.js new file mode 100644 index 00000000..fc662c01 --- /dev/null +++ b/scripts/dev/psm/deploy_vaultManager.js @@ -0,0 +1,44 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let psm = '0x7616c413F29059D5002B0cCdFc2c82526EdA3E23'; +let usdc = '0xadbccCa89eC498F8B9B7F6A4B05206b113676861'; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + + const VaultManager = await hre.ethers.getContractFactory('VaultManager'); + const vaultManager = await upgrades.deployProxy(VaultManager, [ + deployer, + deployer, + psm, + usdc, + deployer + ]); + await vaultManager.waitForDeployment(); + + const proxyAddress = await vaultManager.getAddress(); + + console.log('VaultManager deployed to:', proxyAddress); + try { + await run("verify:verify", { + address: proxyAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + + const psmContract = await ethers.getContractAt('PSM', psm); + + await psmContract.setVaultManager(proxyAddress); + + console.log('VaultManager deploy and setup done'); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/dev/psm/deploy_venusAdapter.js b/scripts/dev/psm/deploy_venusAdapter.js new file mode 100644 index 00000000..5046012e --- /dev/null +++ b/scripts/dev/psm/deploy_venusAdapter.js @@ -0,0 +1,50 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let token = '0xadbccCa89eC498F8B9B7F6A4B05206b113676861'; +let vaultManager = '0x107fCA953BAbc1962A5c29F66aa615a6cf3c99Da'; +let vToken = '0x69D7Bc4A60b342C9811915f9628035A72C81EC60'; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + const admin = deployer; + const manager = deployer; + const feeReceiver = deployer; + + const ListaAdapter = await hre.ethers.getContractFactory('VenusAdapter'); + const listaAdapter = await upgrades.deployProxy(ListaAdapter, [ + admin, + manager, + vaultManager, + token, + vToken, + feeReceiver + ]); + await listaAdapter.waitForDeployment(); + + const proxyAddress = await listaAdapter.getAddress(); + + console.log('VenusAdapter deployed to:', proxyAddress); + try { + await run("verify:verify", { + address: proxyAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + + const vaultManagerContract = await ethers.getContractAt('VaultManager', vaultManager); + + await vaultManagerContract.addAdapter(proxyAddress, 100); + + console.log('VenusAdapter deploy and setup done'); + +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/prod/psm/deploy_earnPool.js b/scripts/prod/psm/deploy_earnPool.js new file mode 100644 index 00000000..21c8c76b --- /dev/null +++ b/scripts/prod/psm/deploy_earnPool.js @@ -0,0 +1,59 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') +const Promise = require("bluebird"); + +let pauser = "0xEEfebb1546d88EA0909435DF6f615084DD3c5Bd8"; +let lisUSD = '0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5'; +let usdt = "0x55d398326f99059fF775485246999027B3197955"; +let zero = "0x0000000000000000000000000000000000000000"; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + const admin = deployer; + const manager = deployer; + const lisUSDPoolAddress = "0x37DB1AE9B24055D1F9fE973Aea40B7EB2995D0Bf"; + const psmAddress = "0xaa57F36DD5Ef2aC471863ec46277f976f272eC0c"; + const EarnPool = await hre.ethers.getContractFactory('EarnPool'); + const earnPoll = await upgrades.deployProxy(EarnPool, [ + admin, + manager, + pauser, + lisUSDPoolAddress, + lisUSD, + ]); + await earnPoll.waitForDeployment(); + + const earnPollAddress = await earnPoll.getAddress(); + + try { + await run("verify:verify", { + address: earnPollAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + console.log('EarnPool deployed to:', earnPollAddress); + + const earnPoolContract = await ethers.getContractAt('EarnPool', earnPollAddress); + + const LisUSDPoolContract = await ethers.getContractAt('LisUSDPoolSet', lisUSDPoolAddress); + + await LisUSDPoolContract.setEarnPool(earnPollAddress); + await Promise.delay(3000); + + earnPoolContract.setPSM(usdt, psmAddress); + await Promise.delay(3000); + + LisUSDPoolContract.registerPool(lisUSD, lisUSD, zero); + await Promise.delay(3000); + + console.log("EarnPool deploy and setup done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/prod/psm/deploy_lisUSDPoolSet.js b/scripts/prod/psm/deploy_lisUSDPoolSet.js new file mode 100644 index 00000000..ee087926 --- /dev/null +++ b/scripts/prod/psm/deploy_lisUSDPoolSet.js @@ -0,0 +1,63 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') +const Promise = require("bluebird"); + +let pauser = "0xEEfebb1546d88EA0909435DF6f615084DD3c5Bd8"; +let lisUSD = '0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5'; +let bot = '0x3995852eb0C4E8b1aA4cB31dDAC254ff199111ff'; +let mutiSigManager = '0x8d388136d578dCD791D081c6042284CED6d9B0c6'; +let maxDuty = '1000000004431822000000000000'; // 15% +let maxAmount = "30000000000000000000000000"; // 30m +let duty = '1000000001847694957439352158'; // 6% +let withdrawDelay = 5; +let BOT_ROLE = '0x902cbe3a02736af9827fb6a90bada39e955c0941e08f0c63b3a662a7b17a4e2b'; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + const admin = deployer; + const manager = deployer; + const LisUSDPool = await hre.ethers.getContractFactory('LisUSDPoolSet'); + const lisUSDPool = await upgrades.deployProxy(LisUSDPool, [ + admin, + manager, + pauser, + bot, + lisUSD, + maxDuty, + withdrawDelay + ]); + await lisUSDPool.waitForDeployment(); + + const lisUSDPoolAddress = await lisUSDPool.getAddress(); + + try { + await run("verify:verify", { + address: lisUSDPoolAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + + const LisUSDPoolContract = await ethers.getContractAt('LisUSDPoolSet', lisUSDPoolAddress); + + await LisUSDPoolContract.setMaxAmount(maxAmount); + await Promise.delay(3000); + await LisUSDPoolContract.grantRole(BOT_ROLE, deployer); + await Promise.delay(3000); + await LisUSDPoolContract.setDuty(duty); + await Promise.delay(3000); + await LisUSDPoolContract.revokeRole(BOT_ROLE, deployer); + await Promise.delay(3000); + await LisUSDPoolContract.grantRole(BOT_ROLE, mutiSigManager); + await Promise.delay(3000); + + console.log('LisUSDPool deployed to:', lisUSDPoolAddress); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/prod/psm/deploy_psm.js b/scripts/prod/psm/deploy_psm.js new file mode 100644 index 00000000..07dcfa27 --- /dev/null +++ b/scripts/prod/psm/deploy_psm.js @@ -0,0 +1,55 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let usdt = '0x55d398326f99059fF775485246999027B3197955'; +let lisUSD = '0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5'; +let sellFee = 0; // 0% +let buyFee = 200; // 2% +let dailyLimit = '500000000000000000000000' // 500k; +let minSell = '1000000000000000000'; // 1u; +let minBuy = '1000000000000000000'; // 1u; +const pauser = "0xEEfebb1546d88EA0909435DF6f615084DD3c5Bd8"; +const feeReceiver = "0x34B504A5CF0fF41F8A480580533b6Dda687fa3Da"; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + const admin = deployer; + const manager = deployer; + const token = usdt; + // deploy PSM (name) + const PSM = await hre.ethers.getContractFactory('PSM'); + const psm = await upgrades.deployProxy(PSM, [ + admin, + manager, + pauser, + token, + feeReceiver, + lisUSD, + sellFee, + buyFee, + dailyLimit, + minSell, + minBuy + ]); + await psm.waitForDeployment(); + + const psmAddress = await psm.getAddress(); + + try { + await run("verify:verify", { + address: psmAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + + console.log(`PSM (USDT) deployed to:`, psmAddress); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/prod/psm/deploy_vaultManager.js b/scripts/prod/psm/deploy_vaultManager.js new file mode 100644 index 00000000..66d2e7db --- /dev/null +++ b/scripts/prod/psm/deploy_vaultManager.js @@ -0,0 +1,44 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let usdt = '0x55d398326f99059fF775485246999027B3197955'; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + const admin = deployer; + const manager = deployer; + const token = usdt; + const psmAddress = "0xaa57F36DD5Ef2aC471863ec46277f976f272eC0c"; + const VaultManager = await hre.ethers.getContractFactory('VaultManager'); + const vaultManager = await upgrades.deployProxy(VaultManager, [ + admin, + manager, + psmAddress, + token + ]); + await vaultManager.waitForDeployment(); + + const vaultManagerAddress = await vaultManager.getAddress(); + + try { + await run("verify:verify", { + address: vaultManagerAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + + const psmContract = await ethers.getContractAt('PSM', psmAddress); + + await psmContract.setVaultManager(vaultManagerAddress); + + console.log(`VaultManager USDT deployed to:`, vaultManagerAddress); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/prod/psm/deploy_venusAdapter.js b/scripts/prod/psm/deploy_venusAdapter.js new file mode 100644 index 00000000..ef2e42b2 --- /dev/null +++ b/scripts/prod/psm/deploy_venusAdapter.js @@ -0,0 +1,51 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let usdt = '0x55d398326f99059fF775485246999027B3197955'; +let vusdt = "0xfD5840Cd36d94D7229439859C0112a4185BC0255"; +let feeReceiver = "0x8d388136d578dCD791D081c6042284CED6d9B0c6"; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + const admin = deployer; + const manager = deployer; + const token = usdt; + const vToken = vusdt; + const vaultManagerAddress = "0x5763DDeB60c82684F3D0098aEa5076C0Da972ec7"; + const VenusAdapter = await hre.ethers.getContractFactory('VenusAdapter'); + const venusAdapter = await upgrades.deployProxy(VenusAdapter, [ + admin, + manager, + vaultManagerAddress, + token, + vToken, + feeReceiver + ]); + + await venusAdapter.waitForDeployment(); + + const venusAdapterAddress = await venusAdapter.getAddress(); + + try { + await run("verify:verify", { + address: venusAdapterAddress, + }); + } catch (error) { + console.error('error verifying contract:', error); + } + console.log(`VenusAdapter USDT deployed to:`, venusAdapterAddress); + + const vaultManagerContract = await ethers.getContractAt('VaultManager', vaultManagerAddress); + + await vaultManagerContract.addAdapter(venusAdapterAddress, 100); + + console.log('VenusAdapter deploy and setup done') +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/prod/psm/transfer_role.js b/scripts/prod/psm/transfer_role.js new file mode 100644 index 00000000..add93f44 --- /dev/null +++ b/scripts/prod/psm/transfer_role.js @@ -0,0 +1,85 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') +const Promise = require("bluebird"); + +let MANAGER_ROLE = '0xaf290d8680820aad922855f39b306097b20e28774d6c1ad35a20325630c3a02c'; +let ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + + const admin = '0x07D274a68393E8b8a2CCf19A2ce4Ba3518735253'; + const manager = '0x8d388136d578dCD791D081c6042284CED6d9B0c6'; + + const psmAddress = '0xaa57F36DD5Ef2aC471863ec46277f976f272eC0c'; + const vaultManagerAddress = '0x5763DDeB60c82684F3D0098aEa5076C0Da972ec7'; + const venusAdapterAddress = '0xf76D9cFD08dF91491680313B1A5b44307129CDa9'; + const lisUSDPoolSetAddress = '0x37DB1AE9B24055D1F9fE973Aea40B7EB2995D0Bf'; + const earnPoolAddress = '0x66dE07893Db7492B56bA88503B4cC99bAb1796F3'; + + const psmContract = await ethers.getContractAt('PSM', psmAddress); + const vaultManagerContract = await ethers.getContractAt('VaultManager', vaultManagerAddress); + const venusAdapterContract = await ethers.getContractAt('VenusAdapter', venusAdapterAddress); + const lisUSDPoolSetContract = await ethers.getContractAt('LisUSDPoolSet', lisUSDPoolSetAddress); + const earnPoolContract = await ethers.getContractAt('EarnPool', earnPoolAddress); + + await Promise.delay(3000); + await psmContract.grantRole(MANAGER_ROLE, manager); + await Promise.delay(3000); + await psmContract.revokeRole(MANAGER_ROLE, deployer); + await Promise.delay(3000); + await psmContract.grantRole(ADMIN_ROLE, admin); + await Promise.delay(3000); + await psmContract.revokeRole(ADMIN_ROLE, deployer); + console.log("psm role setup done"); + + await Promise.delay(3000); + await vaultManagerContract.grantRole(MANAGER_ROLE, manager); + await Promise.delay(3000); + await vaultManagerContract.revokeRole(MANAGER_ROLE, deployer); + await Promise.delay(3000); + await vaultManagerContract.grantRole(ADMIN_ROLE, admin); + await Promise.delay(3000); + await vaultManagerContract.revokeRole(ADMIN_ROLE, deployer); + console.log("vaultManager role setup done"); + + await Promise.delay(3000); + await venusAdapterContract.grantRole(MANAGER_ROLE, manager); + await Promise.delay(3000); + await venusAdapterContract.revokeRole(MANAGER_ROLE, deployer); + await Promise.delay(3000); + await venusAdapterContract.grantRole(ADMIN_ROLE, admin); + await Promise.delay(3000); + await venusAdapterContract.revokeRole(ADMIN_ROLE, deployer); + console.log("venusAdapter role setup done"); + + await Promise.delay(3000); + await lisUSDPoolSetContract.grantRole(MANAGER_ROLE, manager); + await Promise.delay(3000); + await lisUSDPoolSetContract.revokeRole(MANAGER_ROLE, deployer); + await Promise.delay(3000); + await lisUSDPoolSetContract.grantRole(ADMIN_ROLE, admin); + await Promise.delay(3000); + await lisUSDPoolSetContract.revokeRole(ADMIN_ROLE, deployer); + console.log("lisUSDPoolSet role setup done"); + + await Promise.delay(3000); + await earnPoolContract.grantRole(MANAGER_ROLE, manager); + await Promise.delay(3000); + await earnPoolContract.revokeRole(MANAGER_ROLE, deployer); + await Promise.delay(3000); + await earnPoolContract.grantRole(ADMIN_ROLE, admin); + await Promise.delay(3000); + await earnPoolContract.revokeRole(ADMIN_ROLE, deployer); + console.log("earnPool role setup done"); + + console.log('Transfer role done'); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/upgrades/deploy_impl.js b/scripts/upgrades/deploy_impl.js index 7c2c4fa2..efe88a76 100644 --- a/scripts/upgrades/deploy_impl.js +++ b/scripts/upgrades/deploy_impl.js @@ -3,7 +3,7 @@ const {deployImplementation, verifyImpContract} = require('./utils/upgrade_utils const oldContractAddress = '' const oldContractName = '' -const contractName = 'FlashBuy' +const contractName = 'VaultManager' async function main() { diff --git a/test/foundry/LisUSDAcl.t.sol b/test/foundry/LisUSDAcl.t.sol index 0cda0c05..337ae5e7 100644 --- a/test/foundry/LisUSDAcl.t.sol +++ b/test/foundry/LisUSDAcl.t.sol @@ -32,18 +32,12 @@ contract LisUSDAclTest is Test { mainnet = vm.createSelectFork("https://bsc-dataseed.binance.org"); oldLisUSD = LisUSDOld(0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5); - vm.startPrank(0xAca0ed4651ddA1F43f00363643CFa5EBF8774b37); - oldLisUSD.mint(user0, 100 ether); - vm.stopPrank(); - - assertEq(100 ether, oldLisUSD.balanceOf(user0)); - // HayJoin vm.startPrank(0x4C798F81de7736620Cd8e6510158b1fE758e22F7); oldLisUSD.mint(user0, 100 ether); vm.stopPrank(); - assertEq(200 ether, oldLisUSD.balanceOf(user0)); + assertEq(100 ether, oldLisUSD.balanceOf(user0)); ProxyAdmin proxyAdmin = ProxyAdmin(address(0x1Fa3E4718168077975fF4039304CC2e19Ae58c4C)); vm.startPrank(address(proxyAdmin.owner())); @@ -58,18 +52,12 @@ contract LisUSDAclTest is Test { function test_setUp() public { deal(address(lisUSD), user0, 0); - vm.startPrank(0xAca0ed4651ddA1F43f00363643CFa5EBF8774b37); - lisUSD.mint(user0, 100 ether); - vm.stopPrank(); - - assertEq(100 ether, lisUSD.balanceOf(user0)); - // HayJoin vm.startPrank(0x4C798F81de7736620Cd8e6510158b1fE758e22F7); lisUSD.mint(user0, 100 ether); vm.stopPrank(); - assertEq(200 ether, lisUSD.balanceOf(user0)); + assertEq(100 ether, lisUSD.balanceOf(user0)); } function test_rely_minter() public { diff --git a/test/psm/EarnPool.t.sol b/test/psm/EarnPool.t.sol new file mode 100644 index 00000000..11b54d4f --- /dev/null +++ b/test/psm/EarnPool.t.sol @@ -0,0 +1,287 @@ +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "../../contracts/psm/LisUSDPoolSet.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../../contracts/psm/PSM.sol"; +import "../../contracts/psm/VaultManager.sol"; +import "../../contracts/LisUSD.sol"; +import "../../contracts/psm/EarnPool.sol"; + +contract EarnPoolTest is Test { + PSM psm; + VaultManager vaultManager; + LisUSDPoolSet lisUSDPool; + EarnPool earnPool; + address admin = address(0x1); + address user1 = address(0x2); + address lisUSD = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; + address USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; + uint256 MAX_DUTY = 1000000005781378656804590540; + uint256 duty = 1000000005781378656804590540; + address USDT = 0x55d398326f99059fF775485246999027B3197955; + + address lisUSDAuth = 0x07D274a68393E8b8a2CCf19A2ce4Ba3518735253; + + uint256 MAX_UINT = 115792089237316195423570985008687907853269984665640564039457584007913129639935; + + ProxyAdmin lisUSDProxyAdmin = ProxyAdmin(0x1Fa3E4718168077975fF4039304CC2e19Ae58c4C); + + function setUp() public { + vm.createSelectFork("bsc-main"); + + vm.deal(admin, 100 ether); + vm.deal(user1, 100 ether); + deal(lisUSD, admin, 10000000 ether); + + vm.startPrank(admin); + PSM psmImpl = new PSM(); + + ERC1967Proxy psmProxy = new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + admin, + admin, + USDC, + admin, + lisUSD, + 0, + 0, + 1e18 * 1e7, + 1e18, + 1e18 + ) + ); + + psm = PSM(address(psmProxy)); + + VaultManager vaultManagerImpl = new VaultManager(); + + ERC1967Proxy vaultManagerProxy = new ERC1967Proxy( + address(vaultManagerImpl), + abi.encodeWithSelector(vaultManagerImpl.initialize.selector, admin, admin, address(psm), USDC, admin) + ); + + vaultManager = VaultManager(address(vaultManagerProxy)); + + psm.setVaultManager(address(vaultManager)); + + LisUSDPoolSet lisUSDPoolImpl = new LisUSDPoolSet(); + ERC1967Proxy lisUSDPoolProxy = new ERC1967Proxy( + address(lisUSDPoolImpl), + abi.encodeWithSelector(lisUSDPoolImpl.initialize.selector, admin, admin, admin, admin, lisUSD, MAX_DUTY, 0) + ); + + lisUSDPool = LisUSDPoolSet(address(lisUSDPoolProxy)); + + EarnPool earnPoolImpl = new EarnPool(); + ERC1967Proxy earnPoolProxy = new ERC1967Proxy( + address(earnPoolImpl), + abi.encodeWithSelector(earnPoolImpl.initialize.selector, admin, admin, admin, address(lisUSDPool), lisUSD) + ); + earnPool = EarnPool(address(earnPoolProxy)); + + earnPool.setPSM(USDC, address(psm)); + + lisUSDPool.grantRole(lisUSDPool.BOT(), admin); + lisUSDPool.setEarnPool(address(earnPool)); + lisUSDPool.registerPool(USDC, USDC, address(0)); + lisUSDPool.setDuty(duty); + lisUSDPool.setMaxAmount(1e18 * 1e9); + + vm.stopPrank(); + + vm.startPrank(lisUSDAuth); + LisUSD lisUSDImpl = new LisUSD(); + lisUSDProxyAdmin.upgrade(ITransparentUpgradeableProxy(lisUSD), address(lisUSDImpl)); + + LisUSD(lisUSD).rely(address(psm), 1); + LisUSD(lisUSD).rely(address(lisUSDPool), 1); + vm.stopPrank(); + + vm.startPrank(admin); + IERC20(lisUSD).transfer(address(psm), 1000000 ether); + vm.stopPrank(); + } + + function test_initialize() public { + EarnPool earnPoolImpl = new EarnPool(); + + address zero = address(0x0); + + vm.expectRevert("admin cannot be zero address"); + new ERC1967Proxy( + address(earnPoolImpl), + abi.encodeWithSelector(earnPoolImpl.initialize.selector, zero, admin, admin, address(lisUSDPool), lisUSD) + ); + + vm.expectRevert("manager cannot be zero address"); + new ERC1967Proxy( + address(earnPoolImpl), + abi.encodeWithSelector(earnPoolImpl.initialize.selector, admin, zero, admin, address(lisUSDPool), lisUSD) + ); + + vm.expectRevert("pauser cannot be zero address"); + new ERC1967Proxy( + address(earnPoolImpl), + abi.encodeWithSelector(earnPoolImpl.initialize.selector, admin, admin, zero, admin, address(lisUSDPool), lisUSD) + ); + vm.expectRevert("lisUSDPool cannot be zero address"); + new ERC1967Proxy( + address(earnPoolImpl), + abi.encodeWithSelector(earnPoolImpl.initialize.selector, admin, admin, admin, zero, lisUSD) + ); + vm.expectRevert("lisUSD cannot be zero address"); + new ERC1967Proxy( + address(earnPoolImpl), + abi.encodeWithSelector(earnPoolImpl.initialize.selector, admin, admin, admin, address(lisUSDPool), zero) + ); + + assertEq(earnPool.lisUSDPool(), address(lisUSDPool), "lisUSDPool set error"); + assertEq(earnPool.lisUSD(), lisUSD, "lisUSD set error"); + } + + function test_role() public { + EarnPool earnPoolImpl = new EarnPool(); + + ERC1967Proxy earnPoolProxy = new ERC1967Proxy( + address(earnPoolImpl), + abi.encodeWithSelector(earnPoolImpl.initialize.selector, admin, admin, admin, address(lisUSDPool), lisUSD) + ); + + EarnPool earnPool = EarnPool(address(earnPoolProxy)); + + assertTrue(earnPool.hasRole(earnPool.DEFAULT_ADMIN_ROLE(), admin), "admin role error"); + assertTrue(earnPool.hasRole(earnPool.MANAGER(), admin), "manager role error"); + assertTrue(earnPool.hasRole(earnPool.PAUSER(), admin), "pauser role error"); + assertTrue(!earnPool.hasRole(earnPool.PAUSER(), user1), "pauser role error"); + + vm.startPrank(admin); + earnPool.grantRole(earnPool.PAUSER(), user1); + vm.stopPrank(); + + assertTrue(earnPool.hasRole(earnPool.PAUSER(), user1), "pauser role error"); + } + + function test_pause() public { + vm.startPrank(user1); + vm.expectRevert(); + earnPool.pause(); + vm.stopPrank(); + + vm.startPrank(admin); + earnPool.grantRole(earnPool.PAUSER(), user1); + vm.stopPrank(); + + vm.startPrank(user1); + earnPool.pause(); + vm.stopPrank(); + + assertTrue(earnPool.paused(), "paused error"); + + vm.startPrank(admin); + earnPool.unpause(); + vm.stopPrank(); + + assertTrue(!earnPool.paused(), "paused error"); + } + + function test_setPSM() public { + PSM psmImpl = new PSM(); + + ERC1967Proxy psmProxy = new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + admin, + admin, + USDT, + admin, + lisUSD, + 0, + 0, + 1e18 * 1e7, + 1e18, + 1e18 + ) + ); + address usdtPSM = address(psmProxy); + address zero = address(0x0); + + vm.startPrank(user1); + vm.expectRevert(); + earnPool.setPSM(USDT, address(usdtPSM)); + vm.stopPrank(); + + vm.startPrank(admin); + vm.expectRevert("token cannot be zero address"); + earnPool.setPSM(zero, address(usdtPSM)); + vm.expectRevert("psm cannot be zero address"); + earnPool.setPSM(USDT, zero); + vm.expectRevert("psm already set"); + earnPool.setPSM(USDC, address(psm)); + vm.expectRevert("psm token not match"); + earnPool.setPSM(USDT, address(psm)); + + earnPool.setPSM(USDT, address(usdtPSM)); + assertEq(earnPool.psm(USDT), address(usdtPSM), "psm set error"); + + earnPool.removePSM(USDT); + assertEq(earnPool.psm(USDT), address(0), "psm remove error"); + vm.stopPrank(); + } + + function test_depositAndWithdraw() public { + deal(USDC, user1, 1000 ether); + deal(lisUSD, user1, 1000 ether); + + address[] memory pools = new address[](2); + pools[0] = address(USDC); + pools[1] = address(lisUSD); + + vm.startPrank(admin); + IERC20(lisUSD).transfer(address(lisUSDPool), 100 ether); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(USDC).approve(address(earnPool), MAX_UINT); + IERC20(lisUSD).approve(address(earnPool), MAX_UINT); + + earnPool.deposit(USDC, 100 ether); + + uint256 usdcBalance = IERC20(USDC).balanceOf(user1); + uint256 lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + assertEq(usdcBalance, 900 ether, "user1 USDC 0 error"); + assertEq(lisUSDBalance, 1000 ether, "user1 lisUSD 0 error"); + assertEq(IERC20(lisUSD).balanceOf(address(lisUSDPool)), 200 ether, "lisUSDPool lisUSD balance 0 error"); + + skip(1 days); + lisUSDPool.withdraw(pools, 1 ether); + usdcBalance = IERC20(USDC).balanceOf(user1); + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + + uint256 earnPoolBalance = lisUSDPool.poolEmissionWeights(address(USDC), user1); + uint256 totalEmission = lisUSDPool.totalUserEmissionWeights(user1); + assertEq(earnPoolBalance, 99 ether, "user1 earnPool balance 1 error"); + assertEq(usdcBalance, 900 ether, "user1 USDC 1 error"); + assertEq(lisUSDBalance, 1001 ether, "user1 lisUSD 1 error"); + assertEq(totalEmission, 99 ether, "user1 totalEmission 1 error"); + + lisUSDPool.withdraw(pools, 99 ether); + + usdcBalance = IERC20(USDC).balanceOf(user1); + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + earnPoolBalance = lisUSDPool.poolEmissionWeights(address(USDC), user1); + totalEmission = lisUSDPool.totalUserEmissionWeights(user1); + assertEq(earnPoolBalance, 0, "user1 earnPool balance 2 error"); + assertEq(usdcBalance, 900 ether, "user1 USDC 2 error"); + assertEq(lisUSDBalance, 1100 ether, "user1 lisUSD 2 error"); + assertEq(totalEmission, 0, "user1 totalEmission 2 error"); + + vm.stopPrank(); + } +} diff --git a/test/psm/LisUSDPool.t.sol b/test/psm/LisUSDPool.t.sol new file mode 100644 index 00000000..81bfdef5 --- /dev/null +++ b/test/psm/LisUSDPool.t.sol @@ -0,0 +1,267 @@ +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "../../contracts/psm/LisUSDPoolSet.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../../contracts/psm/PSM.sol"; +import "../../contracts/psm/VaultManager.sol"; +import "../../contracts/LisUSD.sol"; + +contract LisUSDPoolTest is Test { + LisUSDPoolSet lisUSDPool; + address admin = address(0x1); + address user1 = address(0x2); + ProxyAdmin proxyAdmin = ProxyAdmin(0xBd8789025E91AF10487455B692419F82523D29Be); + address lisUSD = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; + address USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; + address USDT = 0x55d398326f99059fF775485246999027B3197955; + + address lisUSDAuth = 0x07D274a68393E8b8a2CCf19A2ce4Ba3518735253; + + uint256 MAX_UINT = 115792089237316195423570985008687907853269984665640564039457584007913129639935; + uint256 MAX_DUTY = 1000000005781378656804590540; + + ProxyAdmin lisUSDProxyAdmin = ProxyAdmin(0x1Fa3E4718168077975fF4039304CC2e19Ae58c4C); + + function setUp() public { + vm.createSelectFork("bsc-main"); + + vm.deal(admin, 100 ether); + vm.deal(user1, 100 ether); + + vm.startPrank(admin); + LisUSDPoolSet lisUSDPoolImpl = new LisUSDPoolSet(); + ERC1967Proxy lisUSDPoolProxy = new ERC1967Proxy( + address(lisUSDPoolImpl), + abi.encodeWithSelector(lisUSDPoolImpl.initialize.selector, admin, admin, admin, admin, lisUSD, MAX_DUTY, 0) + ); + + lisUSDPool = LisUSDPoolSet(address(lisUSDPoolProxy)); + + lisUSDPool.grantRole(lisUSDPool.BOT(), admin); + lisUSDPool.setMaxAmount(1e18 * 1e9); + lisUSDPool.setDuty(MAX_DUTY); + lisUSDPool.registerPool(lisUSD, lisUSD, address(0)); + + vm.stopPrank(); + + vm.startPrank(lisUSDAuth); + LisUSD lisUSDImpl = new LisUSD(); + lisUSDProxyAdmin.upgrade(ITransparentUpgradeableProxy(lisUSD), address(lisUSDImpl)); + + LisUSD(lisUSD).rely(address(lisUSDPool), 1); + vm.stopPrank(); + } + + function test_depositAndWithdraw() public { + deal(lisUSD, user1, 100 ether); + deal(lisUSD, admin, 10000 ether); + + address[] memory pools = new address[](1); + pools[0] = address(lisUSD); + + vm.startPrank(admin); + lisUSDPool.setDuty(1000000005781378656804590540); + IERC20(lisUSD).transfer(address(lisUSDPool), 100 ether); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(lisUSD).approve(address(lisUSDPool), MAX_UINT); + + lisUSDPool.deposit(100 ether); + + uint256 lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + uint256 poolBalance = IERC20(lisUSD).balanceOf(address(lisUSDPool)); + uint256 userPoolEmissionWeights = lisUSDPool.poolEmissionWeights(lisUSD, user1); + uint256 userTotalEmissionWeights = lisUSDPool.totalUserEmissionWeights(user1); + + skip(365 days); + + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + poolBalance = IERC20(lisUSD).balanceOf(address(lisUSDPool)); + assertEq(lisUSDBalance, 0, "user1 lisUSD balance 1 error"); + assertEq(poolBalance, 200 ether, "pool lisUSD balance 1 error"); + assertEq(userPoolEmissionWeights, 100 ether, "user1 pool emission weights 1 error"); + assertEq(userTotalEmissionWeights, 100 ether, "user1 total emission weights 1 error"); + + skip(365 days); + + uint256 lisUSDPoolBalance = lisUSDPool.assetBalanceOf(user1); + assertEq( + lisUSDPoolBalance, + (100 ether * lisUSDPool.getRate()) / lisUSDPool.RATE_SCALE(), + "user1 lisUSDPool balance 2 error" + ); + lisUSDPool.withdraw(pools, lisUSDPoolBalance); + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + poolBalance = IERC20(lisUSD).balanceOf(address(lisUSDPool)); + userPoolEmissionWeights = lisUSDPool.poolEmissionWeights(lisUSD, user1); + userTotalEmissionWeights = lisUSDPool.totalUserEmissionWeights(user1); + assertEq( + lisUSDBalance, + (100 ether * lisUSDPool.getRate()) / lisUSDPool.RATE_SCALE(), + "user1 lisUSD balance 2 error" + ); + assertEq( + poolBalance, + 200 ether - (100 ether * lisUSDPool.getRate()) / lisUSDPool.RATE_SCALE(), + "pool lisUSD balance 2 error" + ); + assertEq(userPoolEmissionWeights, 0, "user1 pool emission weights 2 error"); + assertEq(userTotalEmissionWeights, 0, "user1 total emission weights 2 error"); + + vm.stopPrank(); + } + + function test_initialize() public { + LisUSDPoolSet lisUSDPoolImpl = new LisUSDPoolSet(); + + address zero = address(0x0); + + vm.expectRevert("admin cannot be zero address"); + new ERC1967Proxy( + address(lisUSDPoolImpl), + abi.encodeWithSelector(lisUSDPoolImpl.initialize.selector, zero, admin, admin, admin, lisUSD, MAX_DUTY, 0) + ); + + vm.expectRevert("manager cannot be zero address"); + new ERC1967Proxy( + address(lisUSDPoolImpl), + abi.encodeWithSelector(lisUSDPoolImpl.initialize.selector, admin, zero, admin, admin, lisUSD, MAX_DUTY, 0) + ); + + vm.expectRevert("pauser cannot be zero address"); + new ERC1967Proxy( + address(lisUSDPoolImpl), + abi.encodeWithSelector(lisUSDPoolImpl.initialize.selector, admin, admin, zero, admin, lisUSD, MAX_DUTY, 0) + ); + + vm.expectRevert("bot cannot be zero address"); + new ERC1967Proxy( + address(lisUSDPoolImpl), + abi.encodeWithSelector(lisUSDPoolImpl.initialize.selector, admin, admin, admin, zero, lisUSD, MAX_DUTY, 0) + ); + + vm.expectRevert("lisUSD cannot be zero address"); + new ERC1967Proxy( + address(lisUSDPoolImpl), + abi.encodeWithSelector(lisUSDPoolImpl.initialize.selector, admin, admin, admin, admin, zero, MAX_DUTY, 0) + ); + + assertEq(lisUSDPool.lisUSD(), lisUSD, "lisUSD error"); + assertEq(lisUSDPool.maxDuty(), MAX_DUTY, "maxDuty error"); + } + + function test_pause() public { + vm.startPrank(user1); + vm.expectRevert(); + lisUSDPool.pause(); + vm.stopPrank(); + + vm.startPrank(admin); + lisUSDPool.grantRole(lisUSDPool.PAUSER(), user1); + vm.stopPrank(); + + vm.startPrank(user1); + lisUSDPool.pause(); + vm.stopPrank(); + + assertTrue(lisUSDPool.paused(), "paused error"); + + vm.startPrank(admin); + lisUSDPool.unpause(); + vm.stopPrank(); + + assertTrue(!lisUSDPool.paused(), "paused error"); + } + + function test_registerPool() public { + deal(lisUSD, user1, 100 ether); + address zero = address(0x0); + + vm.startPrank(user1); + vm.expectRevert(); + lisUSDPool.registerPool(lisUSD, lisUSD, address(0)); + vm.stopPrank(); + + vm.startPrank(admin); + vm.expectRevert("pool cannot be zero address"); + lisUSDPool.registerPool(zero, lisUSD, address(0)); + vm.expectRevert("asset cannot be zero address"); + lisUSDPool.registerPool(lisUSD, zero, address(0)); + + vm.expectRevert("pool already exists"); + lisUSDPool.registerPool(lisUSD, lisUSD, address(0)); + + lisUSDPool.registerPool(USDT, USDT, address(0)); + + (address asset, address distributor, bool active) = lisUSDPool.pools(USDT); + assertTrue(active, "pool error"); + assertEq(asset, USDT, "asset error"); + assertEq(distributor, address(0), "distributor error"); + + lisUSDPool.removePool(lisUSD); + + (asset, distributor, active) = lisUSDPool.pools(lisUSD); + assertTrue(!active, "pool error"); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(lisUSD).approve(address(lisUSDPool), MAX_UINT); + vm.expectRevert("pool not active"); + lisUSDPool.deposit(10 ether); + vm.stopPrank(); + } + + function test_setMaxAmount() public { + deal(lisUSD, user1, 100 ether); + + vm.startPrank(user1); + vm.expectRevert(); + lisUSDPool.setMaxAmount(1e18 * 1e9); + vm.stopPrank(); + + vm.startPrank(admin); + lisUSDPool.setMaxAmount(1e18); + + assertEq(lisUSDPool.maxAmount(), 1e18, "maxAmount error"); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(lisUSD).approve(address(lisUSDPool), MAX_UINT); + vm.expectRevert("exceed max amount"); + lisUSDPool.deposit(10 ether); + vm.stopPrank(); + } + + function test_withdrawAll() public { + deal(lisUSD, user1, 1000 ether); + deal(lisUSD, admin, 1000 ether); + + vm.startPrank(admin); + lisUSDPool.setWithdrawDelay(5); + IERC20(lisUSD).transfer(address(lisUSDPool), 1000 ether); + vm.stopPrank(); + + address[] memory pools = new address[](1); + pools[0] = lisUSD; + + vm.startPrank(user1); + IERC20(lisUSD).approve(address(lisUSDPool), MAX_UINT); + lisUSDPool.deposit(100 ether); + vm.expectRevert("withdraw delay not reached"); + lisUSDPool.withdraw(pools, 100 ether); + + skip(5); + lisUSDPool.withdraw(pools, 100 ether); + uint256 lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + assertEq(lisUSDBalance, 1000 ether, "user1 lisUSD balance 1 error"); + + uint256 asset = lisUSDPool.assetBalanceOf(user1); + lisUSDPool.withdrawAll(pools); + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + assertEq(lisUSDBalance - asset, 1000 ether, "user1 lisUSD balance 2 error"); + vm.stopPrank(); + } +} diff --git a/test/psm/PSM.t.sol b/test/psm/PSM.t.sol new file mode 100644 index 00000000..9500848d --- /dev/null +++ b/test/psm/PSM.t.sol @@ -0,0 +1,486 @@ +pragma solidity ^0.8.10; + +import "../../contracts/LisUSD.sol"; +import "../../contracts/hMath.sol"; +import "../../contracts/psm/LisUSDPoolSet.sol"; +import "../../contracts/psm/PSM.sol"; +import "../../contracts/psm/VaultManager.sol"; +import "../../contracts/psm/VenusAdapter.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "forge-std/Test.sol"; + +contract PSMTest is Test { + PSM psm; + VaultManager vaultManager; + VenusAdapter venusAdapter; + address admin = address(0x10); + address user1 = address(0x2); + ProxyAdmin proxyAdmin = ProxyAdmin(0xBd8789025E91AF10487455B692419F82523D29Be); + address lisUSD = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; + address USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; + address vUSDC = 0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8; + uint256 quotaAmount = 1e18; + + address lisUSDAuth = 0xAca0ed4651ddA1F43f00363643CFa5EBF8774b37; + + uint256 MAX_UINT = 115792089237316195423570985008687907853269984665640564039457584007913129639935; + + function setUp() public { + vm.createSelectFork("bsc-main"); + + vm.deal(admin, 100 ether); + vm.deal(user1, 100 ether); + + vm.startPrank(admin); + PSM psmImpl = new PSM(); + + ERC1967Proxy psmProxy = new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + admin, + admin, + USDC, + admin, + lisUSD, + 0, + 500, + 1e18 * 1e7, + 1e18, + 1e18 + ) + ); + + psm = PSM(address(psmProxy)); + + VaultManager vaultManagerImpl = new VaultManager(); + + ERC1967Proxy vaultManagerProxy = new ERC1967Proxy( + address(vaultManagerImpl), + abi.encodeWithSelector(vaultManagerImpl.initialize.selector, admin, admin, address(psm), USDC) + ); + + vaultManager = VaultManager(address(vaultManagerProxy)); + + psm.setVaultManager(address(vaultManager)); + + VenusAdapter venusAdapterImpl = new VenusAdapter(); + + ERC1967Proxy venusAdapterProxy = new ERC1967Proxy( + address(venusAdapterImpl), + abi.encodeWithSelector( + venusAdapterImpl.initialize.selector, + admin, + admin, + address(vaultManager), + USDC, + vUSDC, + quotaAmount, + admin + ) + ); + + venusAdapter = VenusAdapter(address(venusAdapterProxy)); + + vaultManager.addAdapter(address(venusAdapter), 100); + + vm.stopPrank(); + + deal(lisUSD, admin, 1000000 ether); + vm.startPrank(admin); + IERC20(lisUSD).transfer(address(psm), 10000 ether); + vm.stopPrank(); + } + + function test_depositAndWithdraw() public { + deal(USDC, user1, 1000 ether); + + vm.startPrank(user1); + IERC20(USDC).approve(address(psm), MAX_UINT); + IERC20(lisUSD).approve(address(psm), MAX_UINT); + + psm.sell(100 ether); + + uint256 usdcBalance = IERC20(USDC).balanceOf(user1); + uint256 lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + assertEq(usdcBalance, 900 ether, "user1 USDC balance 0 error"); + assertEq(lisUSDBalance, 100 ether, "user1 lisUSD balance 0 error"); + + psm.buy(100 ether); + + usdcBalance = IERC20(USDC).balanceOf(user1); + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + assertEq(usdcBalance, 995 ether, "user1 USDC balance 1 error"); + assertEq(lisUSDBalance, 0, "user1 lisUSD balance 1 error"); + + vm.stopPrank(); + } + + function test_initialize() public { + address zero = address(0x0); + PSM psmImpl = new PSM(); + + vm.expectRevert("admin cannot be zero address"); + new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + zero, + admin, + admin, + USDC, + admin, + lisUSD, + 0, + 500, + 1e18 * 10000, + 1e18, + 1e18 + ) + ); + + vm.expectRevert("manager cannot be zero address"); + new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + zero, + admin, + USDC, + admin, + lisUSD, + 0, + 500, + 1e18 * 10000, + 1e18, + 1e18 + ) + ); + + vm.expectRevert("pauser cannot be zero address"); + new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + admin, + zero, + USDC, + admin, + lisUSD, + 0, + 500, + 1e18 * 10000, + 1e18, + 1e18 + ) + ); + + vm.expectRevert("token cannot be zero address"); + new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + admin, + admin, + zero, + admin, + lisUSD, + 0, + 500, + 1e18 * 10000, + 1e18, + 1e18 + ) + ); + + vm.expectRevert("feeReceiver cannot be zero address"); + new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + admin, + admin, + USDC, + zero, + lisUSD, + 0, + 500, + 1e18 * 10000, + 1e18, + 1e18 + ) + ); + + vm.expectRevert("lisUSD cannot be zero address"); + new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + admin, + admin, + USDC, + admin, + zero, + 0, + 500, + 1e18 * 10000, + 1e18, + 1e18 + ) + ); + + vm.expectRevert("sellFee must be less or equal than FEE_PRECISION"); + new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + admin, + admin, + USDC, + admin, + lisUSD, + 1e18, + 500, + 1e18 * 10000, + 1e18, + 1e18 + ) + ); + + vm.expectRevert("buyFee must be less or equal than FEE_PRECISION"); + new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + admin, + admin, + USDC, + admin, + lisUSD, + 0, + 1e18, + 1e18 * 10000, + 1e18, + 1e18 + ) + ); + + vm.expectRevert("dailyLimit must be greater or equal than minBuy"); + new ERC1967Proxy( + address(psmImpl), + abi.encodeWithSelector( + psmImpl.initialize.selector, + admin, + admin, + admin, + USDC, + admin, + lisUSD, + 0, + 500, + 0, + 1e18, + 1e18 + ) + ); + } + + function test_setVaultManager() public { + address zero = address(0x0); + + vm.startPrank(admin); + vm.expectRevert("VaultManager cannot be zero address"); + psm.setVaultManager(zero); + + vm.expectRevert("VaultManager already set"); + psm.setVaultManager(address(vaultManager)); + vm.stopPrank(); + + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(psm.MANAGER()), 32) + ) + ); + psm.setVaultManager(address(vaultManager)); + vm.stopPrank(); + } + + function test_setBuyFee() public { + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(psm.MANAGER()), 32) + ) + ); + psm.setBuyFee(100); + vm.stopPrank(); + + vm.startPrank(admin); + vm.expectRevert("buyFee must be less or equal than FEE_PRECISION"); + psm.setBuyFee(10001); + + psm.setBuyFee(100); + vm.stopPrank(); + + assertEq(psm.buyFee(), 100, "buyFee error"); + } + + function test_setSellFee() public { + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(psm.MANAGER()), 32) + ) + ); + psm.setSellFee(100); + vm.stopPrank(); + + vm.startPrank(admin); + vm.expectRevert("sellFee must be less or equal than FEE_PRECISION"); + psm.setSellFee(10001); + psm.setSellFee(100); + vm.stopPrank(); + + assertEq(psm.sellFee(), 100, "sellFee error"); + } + + function test_setFeeReceiver() public { + address zero = address(0x0); + + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(psm.MANAGER()), 32) + ) + ); + psm.setFeeReceiver(admin); + vm.stopPrank(); + + vm.startPrank(admin); + vm.expectRevert("feeReceiver cannot be zero address"); + psm.setFeeReceiver(zero); + + psm.setFeeReceiver(admin); + vm.stopPrank(); + + assertEq(psm.feeReceiver(), admin, "set feeReceiver error"); + } + + function test_setDailyLimit() public { + uint256 minBuy = psm.minBuy(); + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(psm.MANAGER()), 32) + ) + ); + psm.setDailyLimit(100); + vm.stopPrank(); + + vm.startPrank(admin); + vm.expectRevert("dailyLimit must be greater or equal than minBuy"); + psm.setDailyLimit(minBuy - 1); + + psm.setDailyLimit(minBuy + 1); + vm.stopPrank(); + + assertEq(psm.dailyLimit(), minBuy + 1, "dailyLimit error"); + } + + function test_setMinBuy() public { + uint256 dailyLimit = psm.dailyLimit(); + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(psm.MANAGER()), 32) + ) + ); + psm.setMinBuy(100); + vm.stopPrank(); + + vm.startPrank(admin); + vm.expectRevert("minBuy must be less or equal than dailyLimit"); + psm.setMinBuy(dailyLimit + 1); + + psm.setMinBuy(dailyLimit); + vm.stopPrank(); + + assertEq(psm.minBuy(), dailyLimit, "minBuy error"); + } + + function test_setMinSell() public { + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(psm.MANAGER()), 32) + ) + ); + psm.setMinSell(100); + vm.stopPrank(); + + vm.startPrank(admin); + psm.setMinSell(100); + vm.stopPrank(); + + assertEq(psm.minSell(), 100, "minSell error"); + } + + function test_harvest() public { + deal(USDC, user1, 100 ether); + deal(lisUSD, user1, 100 ether); + + uint256 feeReceiverLisUSDBalance = IERC20(lisUSD).balanceOf(admin); + + vm.startPrank(admin); + psm.setBuyFee(100); + psm.setSellFee(100); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(USDC).approve(address(psm), UINT256_MAX); + IERC20(lisUSD).approve(address(psm), UINT256_MAX); + + psm.sell(100 ether); + assertEq(psm.fees(), 1 ether, "0 fees error"); + + psm.buy(100 ether); + assertEq(psm.fees(), 2 ether, "1 fees error"); + + psm.harvest(); + assertEq(psm.fees(), 0, "2 fees error"); + + assertEq(IERC20(lisUSD).balanceOf(admin), feeReceiverLisUSDBalance + 2 ether, "0 feeReceiver lisUSD balance error"); + vm.stopPrank(); + } +} diff --git a/test/psm/VaultManager.t.sol b/test/psm/VaultManager.t.sol new file mode 100644 index 00000000..8a6d5125 --- /dev/null +++ b/test/psm/VaultManager.t.sol @@ -0,0 +1,283 @@ +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "../../contracts/psm/LisUSDPoolSet.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../../contracts/psm/PSM.sol"; +import "../../contracts/psm/VaultManager.sol"; +import "../../contracts/psm/VenusAdapter.sol"; +import "../../contracts/LisUSD.sol"; +import "../../contracts/hMath.sol"; + +contract VaultManagerTest is Test { + VaultManager vaultManager; + VenusAdapter venusAdapter; + address admin = address(0x10); + address user1 = address(0x2); + ProxyAdmin proxyAdmin = ProxyAdmin(0xBd8789025E91AF10487455B692419F82523D29Be); + address lisUSD = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; + address USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; + address vUSDC = 0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8; + uint256 quotaAmount = 1e18; + + address lisUSDAuth = 0xAca0ed4651ddA1F43f00363643CFa5EBF8774b37; + + uint256 MAX_UINT = 115792089237316195423570985008687907853269984665640564039457584007913129639935; + + function setUp() public { + vm.createSelectFork("bsc-main"); + + vm.deal(admin, 100 ether); + vm.deal(user1, 100 ether); + + vm.startPrank(admin); + + VaultManager vaultManagerImpl = new VaultManager(); + + ERC1967Proxy vaultManagerProxy = new ERC1967Proxy( + address(vaultManagerImpl), + abi.encodeWithSelector(vaultManagerImpl.initialize.selector, admin, admin, address(user1), USDC) + ); + + vaultManager = VaultManager(address(vaultManagerProxy)); + + VenusAdapter venusAdapterImpl = new VenusAdapter(); + + ERC1967Proxy venusAdapterProxy = new ERC1967Proxy( + address(venusAdapterImpl), + abi.encodeWithSelector( + venusAdapterImpl.initialize.selector, + admin, + admin, + address(vaultManager), + USDC, + vUSDC, + quotaAmount, + admin + ) + ); + + venusAdapter = VenusAdapter(address(venusAdapterProxy)); + + vm.stopPrank(); + } + + function test_depositAndWithdraw() public { + deal(USDC, user1, 1000 ether); + + vm.startPrank(admin); + vaultManager.addAdapter(address(venusAdapter), 100); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(USDC).approve(address(vaultManager), MAX_UINT); + + vaultManager.deposit(100 ether); + + uint256 usdcBalance = IERC20(USDC).balanceOf(user1); + assertEq(usdcBalance, 900 ether, "user1 USDC 0 error"); + + vaultManager.withdraw(user1, 99 ether); + usdcBalance = IERC20(USDC).balanceOf(user1); + assertEq(usdcBalance, 999 ether, "user1 USDC 1 error"); + vm.stopPrank(); + } + + function test_addAdapter() public { + deal(USDC, user1, 1000 ether); + + vm.startPrank(admin); + vaultManager.addAdapter(address(venusAdapter), 1000); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(USDC).approve(address(vaultManager), MAX_UINT); + + vaultManager.deposit(1000 ether); + + uint256 venusAdapterBalance = IVBep20Delegate(vUSDC).balanceOfUnderlying(address(venusAdapter)); + uint256 vaultManagerBalance = IERC20(USDC).balanceOf(address(vaultManager)); + assertTrue(venusAdapterBalance <= 1000 ether && venusAdapterBalance > 999 ether, "venusAdapterBalance 0 error"); + assertEq(vaultManagerBalance, 0, "vaultManagerBalance 0 error"); + + vaultManager.withdraw(user1, 900 ether); + venusAdapterBalance = IVBep20Delegate(vUSDC).balanceOfUnderlying(address(venusAdapter)); + vaultManagerBalance = IERC20(USDC).balanceOf(address(vaultManager)); + assertTrue(venusAdapterBalance <= 101 ether && venusAdapterBalance > 99 ether, "venusAdapterBalance 1 error"); + assertEq(vaultManagerBalance, 0, "vaultManagerBalance 1 error"); + + vm.stopPrank(); + } + + function test_setAdapter() public { + vm.startPrank(admin); + vaultManager.addAdapter(address(venusAdapter), 100); + vm.stopPrank(); + + (, bool active, uint256 point) = vaultManager.adapters(0); + assertTrue(active, "0 adapter active error"); + assertEq(point, 100, "0 adapter point error"); + + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(vaultManager.MANAGER()), 32) + ) + ); + vaultManager.setAdapter(0, false, 0); + vm.stopPrank(); + + vm.startPrank(admin); + vaultManager.setAdapter(0, false, 10); + vm.stopPrank(); + + (, active, point) = vaultManager.adapters(0); + assertTrue(!active, "1 adapter active error"); + assertEq(point, 10, "1 adapter point error"); + } + + function test_rebalance() public { + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(vaultManager.BOT()), 32) + ) + ); + + vaultManager.rebalance(); + vm.stopPrank(); + + vm.startPrank(admin); + vaultManager.grantRole(vaultManager.BOT(), admin); + + vm.expectRevert("no adapter"); + vaultManager.rebalance(); + + vaultManager.addAdapter(address(venusAdapter), 100); + vaultManager.rebalance(); + vm.stopPrank(); + } + + function test_emergencyWithdraw() public { + deal(USDC, user1, 1000 ether); + + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(vaultManager.DEFAULT_ADMIN_ROLE()), 32) + ) + ); + + vaultManager.emergencyWithdraw(0); + vm.stopPrank(); + + vm.startPrank(admin); + vaultManager.addAdapter(address(venusAdapter), 100); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(USDC).approve(address(vaultManager), MAX_UINT); + + vaultManager.deposit(100 ether); + vm.stopPrank(); + + vm.startPrank(admin); + vaultManager.emergencyWithdraw(0); + vm.stopPrank(); + + uint256 usdcBalance = IERC20(USDC).balanceOf(address(admin)); + assertTrue(usdcBalance <= 100 ether && usdcBalance >= 100 ether - 1000000000, "admin USDC 0 error"); + } + + function test_initialize() public { + address zero = address(0x0); + VaultManager vaultManagerImpl = new VaultManager(); + + vm.expectRevert("admin cannot be zero address"); + new ERC1967Proxy( + address(vaultManagerImpl), + abi.encodeWithSelector(vaultManagerImpl.initialize.selector, zero, admin, admin, USDC) + ); + + vm.expectRevert("manager cannot be zero address"); + new ERC1967Proxy( + address(vaultManagerImpl), + abi.encodeWithSelector(vaultManagerImpl.initialize.selector, admin, zero, admin, USDC) + ); + + vm.expectRevert("psm cannot be zero address"); + new ERC1967Proxy( + address(vaultManagerImpl), + abi.encodeWithSelector(vaultManagerImpl.initialize.selector, admin, admin, zero, USDC) + ); + + vm.expectRevert("token cannot be zero address"); + new ERC1967Proxy( + address(vaultManagerImpl), + abi.encodeWithSelector(vaultManagerImpl.initialize.selector, admin, admin, admin, zero) + ); + } + + function test_gas() public { + deal(USDC, user1, 1000 ether); + + address adapter1 = createAdapter(); + address adapter2 = createAdapter(); + + vm.startPrank(admin); + vaultManager.addAdapter(adapter1, 100); + vaultManager.addAdapter(adapter2, 100); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(USDC).approve(address(vaultManager), MAX_UINT); + + vaultManager.deposit(100 ether); + + vaultManager.withdraw(user1, 50 ether); + vm.stopPrank(); + + vm.startPrank(admin); + uint256 startIdx = block.number % 2; + vaultManager.setAdapter(startIdx, false, 0); + vm.stopPrank(); + + vm.startPrank(user1); + vaultManager.withdraw(user1, 50 ether); + vm.stopPrank(); + } + + function createAdapter() private returns (address) { + vm.startPrank(admin); + VenusAdapter venusAdapterImpl = new VenusAdapter(); + + ERC1967Proxy venusAdapterProxy = new ERC1967Proxy( + address(venusAdapterImpl), + abi.encodeWithSelector( + venusAdapterImpl.initialize.selector, + admin, + admin, + address(vaultManager), + USDC, + vUSDC, + quotaAmount, + admin + ) + ); + + address adapter = address(venusAdapterProxy); + + vm.stopPrank(); + return adapter; + } +} diff --git a/test/psm/VenusAdapter.t.sol b/test/psm/VenusAdapter.t.sol new file mode 100644 index 00000000..bb26ded4 --- /dev/null +++ b/test/psm/VenusAdapter.t.sol @@ -0,0 +1,172 @@ +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "../../contracts/psm/VenusAdapter.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +contract VenusAdapterTest is Test { + VenusAdapter venusAdapter; + address admin = address(0x10); + address user1 = address(0x004319Fd76912890F7920aEE99Df27EBA05ef48D); + address USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; + address vUSDC = 0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8; + ProxyAdmin proxyAdmin = ProxyAdmin(0xBd8789025E91AF10487455B692419F82523D29Be); + uint256 quotaAmount = 1e18; + + function setUp() public { + vm.createSelectFork("bsc-main"); + + vm.deal(admin, 100 ether); + vm.deal(user1, 100 ether); + + vm.startPrank(admin); + VenusAdapter venusAdapterImpl = new VenusAdapter(); + + ERC1967Proxy venusAdapterProxy = new ERC1967Proxy( + address(venusAdapterImpl), + abi.encodeWithSelector(venusAdapterImpl.initialize.selector, admin, admin, user1, USDC, vUSDC, admin) + ); + + venusAdapter = VenusAdapter(address(venusAdapterProxy)); + + vm.stopPrank(); + } + + function test_depositAndWithdraw() public { + deal(USDC, user1, 200 ether); + + uint256 vUSDCBalance = IERC20(vUSDC).balanceOf(address(venusAdapter)); + assertEq(vUSDCBalance, 0, "vUSDC 0 error"); + + vm.startPrank(user1); + IERC20(USDC).approve(address(venusAdapter), 100 ether); + venusAdapter.deposit(100 ether); + vm.stopPrank(); + + vUSDCBalance = IERC20(vUSDC).balanceOf(address(venusAdapter)); + uint256 gemAmount = IVBep20Delegate(vUSDC).balanceOfUnderlying(address(venusAdapter)); + assertTrue(vUSDCBalance > 0, "vUSDC 1 error"); + assertTrue(gemAmount > 99 ether && gemAmount <= 100 ether, "Staked USDC 1 error"); + + vm.startPrank(user1); + venusAdapter.withdraw(user1, 99 ether); + vm.stopPrank(); + + uint256 USDCBalance = IERC20(USDC).balanceOf(user1); + assertEq(USDCBalance, 199 ether, "user1 USDC 2 error"); + + // console.log("block1", block.number); + // vm.roll(block.number + 10000); + // console.log("block2", block.number); + // + // IVBep20Delegate(venusPool).accrueInterest(); + // gemAmount = venusAdapter.totalAvailableAmount(); + // console.log("Staked USDC:: ", gemAmount); + } + + function test_withdrawAll() public { + deal(USDC, user1, 1000 ether); + + uint256 USDCBalance = IERC20(USDC).balanceOf(user1); + assertEq(USDCBalance, 1000 ether, "user1 USDC 0 error"); + + vm.startPrank(user1); + IERC20(USDC).approve(address(venusAdapter), 100 ether); + venusAdapter.deposit(100 ether); + + venusAdapter.withdrawAll(); + vm.stopPrank(); + + USDCBalance = IERC20(USDC).balanceOf(user1); + assertTrue(USDCBalance <= 1000 ether && USDCBalance >= 999 ether, "user1 USDC 1 error"); + } + + function test_initialize() public { + address zero = address(0x0); + + VenusAdapter venusAdapterImpl = new VenusAdapter(); + + vm.expectRevert("admin cannot be zero address"); + new ERC1967Proxy( + address(venusAdapterImpl), + abi.encodeWithSelector(venusAdapterImpl.initialize.selector, zero, admin, admin, USDC, vUSDC, admin) + ); + + vm.expectRevert("manager cannot be zero address"); + new ERC1967Proxy( + address(venusAdapterImpl), + abi.encodeWithSelector(venusAdapterImpl.initialize.selector, admin, zero, admin, USDC, vUSDC, admin) + ); + + vm.expectRevert("vaultManager cannot be zero address"); + new ERC1967Proxy( + address(venusAdapterImpl), + abi.encodeWithSelector(venusAdapterImpl.initialize.selector, admin, admin, zero, USDC, vUSDC, admin) + ); + + vm.expectRevert("token cannot be zero address"); + new ERC1967Proxy( + address(venusAdapterImpl), + abi.encodeWithSelector(venusAdapterImpl.initialize.selector, admin, admin, admin, zero, vUSDC, admin) + ); + + vm.expectRevert("vToken cannot be zero address"); + new ERC1967Proxy( + address(venusAdapterImpl), + abi.encodeWithSelector(venusAdapterImpl.initialize.selector, admin, admin, admin, USDC, zero, admin) + ); + + vm.expectRevert("feeReceiver cannot be zero address"); + new ERC1967Proxy( + address(venusAdapterImpl), + abi.encodeWithSelector(venusAdapterImpl.initialize.selector, admin, admin, admin, USDC, vUSDC, zero) + ); + } + + function test_harvest() public { + deal(USDC, user1, 100 ether); + vm.startPrank(user1); + IERC20(USDC).approve(address(venusAdapter), UINT256_MAX); + venusAdapter.deposit(100 ether); + + assertEq(IERC20(USDC).balanceOf(user1), 0, "user1 0 USDC balance error"); + assertEq(IERC20(USDC).balanceOf(admin), 0, "admin 0 USDC balance error"); + + vm.roll(block.number + 10000); + venusAdapter.harvest(); + assertEq(IERC20(USDC).balanceOf(user1), 0, "user1 1 USDC balance error"); + assertTrue(IERC20(USDC).balanceOf(admin) > 0, "admin 1 USDC balance error"); + + vm.roll(block.number + 10000); + venusAdapter.withdrawAll(); + assertTrue(IERC20(USDC).balanceOf(user1) > 100 ether, "user1 2 USDC balance error"); + + vm.stopPrank(); + } + + function test_setFeeReceiver() public { + address feeReceiver = address(0x20); + address zero = address(0x0); + + vm.startPrank(user1); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user1), + " is missing role ", + StringsUpgradeable.toHexString(uint256(venusAdapter.MANAGER()), 32) + ) + ); + venusAdapter.setFeeReceiver(feeReceiver); + vm.stopPrank(); + + vm.startPrank(admin); + vm.expectRevert("feeReceiver cannot be zero address"); + venusAdapter.setFeeReceiver(zero); + + venusAdapter.setFeeReceiver(feeReceiver); + vm.stopPrank(); + assertEq(venusAdapter.feeReceiver(), feeReceiver, "feeReceiver set error"); + } +} diff --git a/yarn.lock b/yarn.lock index b2c2abdf..f531bc89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -800,6 +800,13 @@ debug "^4.1.1" lodash.isequal "^4.5.0" +"@nomicfoundation/hardhat-foundry@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.1.2.tgz#4f5aaa1803b8f5d974dcbc361beb72d49c815562" + integrity sha512-f5Vhj3m2qvKGpr6NAINYwNgILDsai8dVCsFb1rAVLkJxOmD2pAtfCmOH5SBVr9yUI5B1z9rbTwPBJVrqnb+PXQ== + dependencies: + chalk "^2.4.2" + "@nomicfoundation/hardhat-network-helpers@^1.0.0": version "1.0.10" resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.10.tgz#c61042ceb104fdd6c10017859fdef6529c1d6585" @@ -1836,7 +1843,7 @@ blakejs@^1.1.0: resolved "https://registry.npmjs.org/blakejs/-/blakejs-1.1.1.tgz" integrity sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg== -bluebird@^3.4.7, bluebird@^3.5.0: +bluebird@^3.4.7, bluebird@^3.5.0, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==