diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..888d42dc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/contracts/hMath.sol b/contracts/hMath.sol index fb96ea20..274d4879 100644 --- a/contracts/hMath.sol +++ b/contracts/hMath.sol @@ -5,7 +5,7 @@ library hMath { uint256 constant ONE = 10 ** 27; - function rpow(uint x, uint n, uint b) internal pure returns (uint z) { + function rpow(uint256 x, uint256 n, uint256 b) internal pure returns (uint256 z) { assembly { switch x case 0 {switch n case 0 {z := b} default {z := 0}} default { @@ -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..a6f770e9 --- /dev/null +++ b/contracts/interfaces/IAdapter.sol @@ -0,0 +1,11 @@ +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); +} \ No newline at end of file diff --git a/contracts/interfaces/IEarnPool.sol b/contracts/interfaces/IEarnPool.sol new file mode 100644 index 00000000..df75abb6 --- /dev/null +++ b/contracts/interfaces/IEarnPool.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.8.10; + +interface IEarnPool { + function deposit(address account, uint256 gemAmount, uint256 lisUSDAmount) external; + + function withdrawLisUSD(address account, uint256 amount) external returns (uint256); + + function withdrawGem(address account, uint256 amount) external returns (uint256); +} \ No newline at end of file diff --git a/contracts/interfaces/ILisUSDPool.sol b/contracts/interfaces/ILisUSDPool.sol new file mode 100644 index 00000000..55590576 --- /dev/null +++ b/contracts/interfaces/ILisUSDPool.sol @@ -0,0 +1,5 @@ +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..8de7676d --- /dev/null +++ b/contracts/interfaces/IPSM.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.10; + +interface IPSM { + function buy(uint256 amount) external; + + function sell(uint256 amount) external; +} \ No newline at end of file diff --git a/contracts/interfaces/IVBep20Delegate.sol b/contracts/interfaces/IVBep20Delegate.sol new file mode 100644 index 00000000..bf1802eb --- /dev/null +++ b/contracts/interfaces/IVBep20Delegate.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.8.10; + +interface IVBep20Delegate { + function mint(uint256 mintAmount) external returns (uint256); + + function redeem(uint256 redeemTokens) external returns (uint256); + + function exchangeRateStored() external view returns (uint256); + + function accrueInterest() external returns (uint256); + + function borrow(uint256 borrowAmount) external returns (uint256); + + function repayBorrow(uint256 repayAmount) 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..4ede3996 --- /dev/null +++ b/contracts/interfaces/IVaultManager.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.10; + +interface IVaultManager { + function deposit(uint256 amount) external; + + function withdraw(address receiver, uint256 amount) external; +} \ 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/psm/EarnPool.sol b/contracts/psm/EarnPool.sol new file mode 100644 index 00000000..9e9c272a --- /dev/null +++ b/contracts/psm/EarnPool.sol @@ -0,0 +1,120 @@ +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "../interfaces/ILisUSDPool.sol"; +import "../interfaces/IPSM.sol"; +import "../interfaces/IStakeLisUSDListaDistributor.sol"; + +contract EarnPool is AccessControlUpgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable { + 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 PAUSE = keccak256("PAUSE"); // pause role + + event SetLisUSDPool(address lisUSDPool); + event SetLisUSD(address lisUSD); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @dev initialize contract + * @param _lisUSDPool lisUSD pool address + * @param _lisUSD lisUSD address + */ + function initialize( + address _lisUSDPool, + address _lisUSD + ) public initializer { + require(_lisUSDPool != address(0), "lisUSDPool cannot be zero address"); + require(_lisUSD != address(0), "lisUSD cannot be zero address"); + __AccessControl_init(); + __ReentrancyGuard_init(); + __Pausable_init(); + + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setupRole(MANAGER, msg.sender); + + lisUSDPool = _lisUSDPool; + lisUSD = _lisUSD; + + emit SetLisUSDPool(_lisUSDPool); + emit SetLisUSD(_lisUSD); + } + + /** + * @dev deposit token to earn pool + * @param amount token amount + */ + function deposit(address token, uint256 amount) external nonReentrant 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(PAUSE) { + _pause(); + } + + /** + * @dev toggle pause contract + */ + function togglePause() external onlyRole(DEFAULT_ADMIN_ROLE) { + if (paused()) { + _unpause(); + } else { + _pause(); + } + } + + /** + * @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"); + psm[_token] = _psm; + } + + /** + * @dev remove psm + * @param _token token address + */ + function removePSM(address _token) external onlyRole(MANAGER) { + delete psm[_token]; + } + + +} \ No newline at end of file diff --git a/contracts/psm/LisUSDPool.sol b/contracts/psm/LisUSDPool.sol new file mode 100644 index 00000000..dc56eaf6 --- /dev/null +++ b/contracts/psm/LisUSDPool.sol @@ -0,0 +1,332 @@ +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 "../hMath.sol"; +import "../interfaces/IStakeLisUSDListaDistributor.sol"; +import "../interfaces/VatLike.sol"; + +contract LisUSDPool is AccessControlUpgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable { + using SafeERC20 for IERC20; + + struct Pool { + address asset; + address distributor; + bool active; + } + + address public lisUSD; // lisUSD address + mapping(address => uint256) public shares; // user's share + // pool => user => balance + mapping(address => mapping(address => uint256)) public poolBalanceOf; + // pool => lista distributor + mapping(address => Pool) public pools; + uint256 public totalShares; // total + string public name; // pool name + string public symbol; // pool symbol + uint256 public rate; // share to lisUSD rate when last update + uint256 public lastUpdate; // last tps update time + uint256 public duty; // rate per second + uint256 public MAX_DUTY; // max rate per second + address public earnPool; // earn pool address + address public vat; // vat address + + bytes32 public constant MANAGER = keccak256("MANAGER"); // manager role + bytes32 public constant PAUSE = keccak256("PAUSE"); // pause role + uint256 constant public RAY = 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); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + modifier onlyEarnPool() { + require(earnPool == msg.sender, "only earnPool can call this function"); + _; + } + + /** + * @dev initialize contract + * @param _lisUSD lisUSD address + * @param maxDuty max rate per second + */ + function initialize( + address _lisUSD, + address _vat, + uint256 maxDuty + ) public initializer { + require(_lisUSD != address(0), "lisUSD cannot be zero address"); + require(_vat != address(0), "vat cannot be zero address"); + + __AccessControl_init(); + __ReentrancyGuard_init(); + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setupRole(MANAGER, msg.sender); + + lisUSD = _lisUSD; + vat = _vat; + + name = "lisUSD single staking Pool"; + symbol = "sLisUSD"; + MAX_DUTY = maxDuty; + + registerPool(lisUSD, lisUSD, address(0)); + + emit SetMaxDuty(maxDuty); + } + + /** + * @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 = balanceToShare(amount); + if (share * getRate() < amount * RAY) { + share += 1; + } + + require(share <= shares[account], "insufficient balance"); + + // update shares + shares[account] -= share; + totalShares -= share; + + // update pool balance + uint256 remain = amount; + for (uint i = 0; i < _pools.length; i++) { + uint256 poolBalance = poolBalanceOf[_pools[i]][account]; + if (poolBalance >= remain) { + poolBalanceOf[_pools[i]][account] -= remain; + takeSnapshot(account, _pools[i]); + break; + } else { + remain -= poolBalance; + poolBalanceOf[_pools[i]][account] = 0; + takeSnapshot(account, _pools[i]); + } + } + + // transfer lisUSD to account + require(IERC20(lisUSD).balanceOf(address(this)) >= amount, "not enough balance"); + IERC20(lisUSD).safeTransfer(account, amount); + + emit Withdraw(account, amount); + } + + + /** + * @dev get user's balance + * @param account account address + */ + function balanceOfAssets(address account) public view returns (uint256) { + return shareToBalance(shares[account]); + } + + /** + * @dev get total lisUSD staked + */ + function totalAssets() public view returns (uint256) { + return shareToBalance(totalShares); + } + + /** + * @dev deposit lisUSD + * @param account account address + * @param amount amount to deposit + */ + function depositFor(address pool, address account, uint256 amount) external onlyEarnPool nonReentrant whenNotPaused { + _depositFor(msg.sender, pool, account, amount); + } + + /** + * @dev deposit lisUSD + * @param amount amount to deposit + */ + function deposit(uint256 amount) external nonReentrant whenNotPaused { + _depositFor(msg.sender, lisUSD, msg.sender, amount); + + } + + function _depositFor(address sender, address pool, address account, uint256 amount) private update { + require(amount > 0, "amount cannot be zero"); + require(totalAssets() + amount <= VatLike(vat).debt() / RAY, "exceed vat debt"); + 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 = balanceToShare(amount); + shares[account] += share; + totalShares += share; + poolBalanceOf[pool][account] += amount; + + takeSnapshot(account, pool); + + emit Deposit(account, amount); + } + + /** + * @dev share to lisUSD + * @param share share + */ + function shareToBalance(uint256 share) public view returns (uint256) { + return Math.mulDiv(share, getRate(), RAY); + } + + /** + * @dev lisUSD to share + * @param balance balance + */ + function balanceToShare(uint256 balance) public view returns (uint256) { + return Math.mulDiv(balance, RAY, 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 RAY; + } + 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) external update onlyRole(MANAGER) { + require(_duty <= MAX_DUTY, "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) { + MAX_DUTY = _maxDuty; + emit SetMaxDuty(_maxDuty); + } + + + /** + * @dev emergency withdraw all lisUSD + */ + function emergencyWithdraw() external onlyRole(DEFAULT_ADMIN_ROLE) { + IERC20(lisUSD).safeTransfer(msg.sender, IERC20(lisUSD).balanceOf(address(this))); + } + + /** + * @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; + } + + + /** + * @dev pause contract + */ + function pause() external onlyRole(PAUSE) { + _pause(); + } + + /** + * @dev toggle pause contract + */ + function togglePause() external onlyRole(DEFAULT_ADMIN_ROLE) { + if (paused()) { + _unpause(); + } else { + _pause(); + } + } + + /** + * @dev take snapshot of user's LisUSD staking amount + * @param user user address + * @param pool pool address + */ + function takeSnapshot(address user, address pool) private { + address distributor = pools[pool].distributor; + // ensure the distributor address is set + if (distributor != address(0)) { + IStakeLisUSDListaDistributor(distributor).takeSnapshot(user, poolBalanceOf[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; + } + + /** + * @dev remove distributor address + * @param pool pool address + */ + function removeDistributor(address pool) external onlyRole(MANAGER) { + pools[pool].distributor = address(0); + } + + /** + * @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); + } + + /** + * @dev remove pool + * @param pool pool address + */ + function removePool(address pool) external onlyRole(MANAGER) { + require(pools[pool].active, "pool not exists"); + + delete pools[pool]; + + emit RemovePool(pool); + } +} \ No newline at end of file diff --git a/contracts/psm/ListaAdapter.sol b/contracts/psm/ListaAdapter.sol new file mode 100644 index 00000000..4f2faca1 --- /dev/null +++ b/contracts/psm/ListaAdapter.sol @@ -0,0 +1,129 @@ +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "../interfaces/IVBep20Delegate.sol"; + +contract ListaAdapter is AccessControlUpgradeable { + using SafeERC20 for IERC20; + address public token; // token address + address public vaultManager; // vault manager address + + uint256 public totalAvailableAmount; // total available amount + uint256 public operatorWithdraw; // operator withdraw amount + + bytes32 public constant MANAGER = keccak256("MANAGER"); // manager role + + event Deposit(uint256 amount); + event Withdraw(address account, uint256 amount); + event OperatorDeposit(address account, uint256 amount); + event OperatorWithdraw(address account, uint256 amount); + event AddOperator(address operator); + event RemoveOperator(address operator); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + modifier onlyVaultManager() { + require(msg.sender == vaultManager, "only vaultManager can call this function"); + _; + } + + /** + * @dev initialize contract + * @param _token token address + * @param _vaultManager vault manager address + */ + function initialize(address _token, address _vaultManager) public initializer { + require(_vaultManager != address(0), "vaultManager cannot be zero address"); + require(_token != address(0), "token cannot be zero address"); + __AccessControl_init(); + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setupRole(MANAGER, msg.sender); + + token = _token; + vaultManager = _vaultManager; + } + + /** + * @dev deposit token by vault manager + * @param amount deposit amount + */ + function deposit(uint256 amount) external onlyVaultManager { + require(amount > 0, "deposit amount cannot be zero"); + // transfer token from vault manager to this contract + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + totalAvailableAmount += amount; + + emit Deposit(amount); + } + + /** + * @dev withdraw token by vault manager + * @param account account address + * @param amount deposit amount + */ + function withdraw(address account, uint256 amount) external onlyVaultManager { + require(amount > 0, "withdraw amount cannot be zero"); + require(totalAvailableAmount >= amount, "insufficient balance"); + + // withdraw from total available amount + totalAvailableAmount -= amount; + IERC20(token).safeTransfer(account, amount); + + emit Withdraw(account, amount); + } + + /** + * @dev withdraw token by operator + * @param amount withdraw amount + */ + function withdrawByOperator(uint256 amount) external onlyRole(MANAGER) { + require(amount > 0, "withdraw amount cannot be zero"); + require(totalAvailableAmount >= amount, "insufficient balance"); + + // withdraw from total available amount and add to operator withdraw + totalAvailableAmount -= amount; + operatorWithdraw += amount; + + IERC20(token).safeTransfer(msg.sender, amount); + + emit OperatorWithdraw(msg.sender, amount); + } + + /** + * @dev deposit token by operator + * @param amount deposit amount + */ + function depositByOperator(uint256 amount) external onlyRole(MANAGER) { + require(amount > 0, "deposit amount cannot be zero"); + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + // add operatorWithdraw to total available amount + totalAvailableAmount += amount; + if(amount >= operatorWithdraw) { + operatorWithdraw = 0; + } else { + operatorWithdraw -= amount; + } + + emit OperatorDeposit(msg.sender, amount); + } + + /** + * @dev withdraw all token to vault manager + */ + function withdrawAll() external onlyVaultManager returns (uint256) { + if (totalAvailableAmount > 0) { + uint256 amount = totalAvailableAmount; + totalAvailableAmount = 0; + IERC20(token).safeTransfer(vaultManager, amount); + return amount; + } + return 0; + } + +} \ No newline at end of file diff --git a/contracts/psm/PSM.sol b/contracts/psm/PSM.sol new file mode 100644 index 00000000..2fd28d89 --- /dev/null +++ b/contracts/psm/PSM.sol @@ -0,0 +1,329 @@ +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/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "../interfaces/IVaultManager.sol"; +import "../interfaces/HayLike.sol"; + +contract PSM is AccessControlUpgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable { + 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 sellLimit; // total sell limit + uint256 public buyLimit; // total buy limit + 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 totalBuyUsed; // total buy used + uint256 public totalSellUsed; // total sell used + + uint256 public constant FEE_PRECISION = 10000; + + bytes32 public constant MANAGER = keccak256("MANAGER"); // manager role + bytes32 public constant PAUSE = keccak256("PAUSE"); // 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 SetSellLimit(uint256 sellLimit); + event SetBuyLimit(uint256 buyLimit); + event SetDailyLimit(uint256 dailyLimit); + event SetMinSell(uint256 minSell); + event SetMinBuy(uint256 minBuy); + event SetVaultManager(address vaultManager); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @dev initialize contract + * @param _token token address + * @param _feeReceiver fee receiver address + * @param _lisUSD lisUSD address + * @param _sellFee sell fee + * @param _buyFee buy fee + * @param _sellLimit sell limit + * @param _buyLimit buy limit + * @param _minSell min sell amount + * @param _minBuy min buy amount + */ + function initialize( + address _token, + address _feeReceiver, + address _lisUSD, + uint256 _sellFee, + uint256 _buyFee, + uint256 _sellLimit, + uint256 _buyLimit, + uint256 _dailyLimit, + uint256 _minSell, + uint256 _minBuy + ) public initializer { + 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"); + __AccessControl_init(); + __ReentrancyGuard_init(); + __Pausable_init(); + + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setupRole(MANAGER, msg.sender); + + token = _token; + sellFee = _sellFee; + buyFee = _buyFee; + feeReceiver = _feeReceiver; + lisUSD = _lisUSD; + + sellLimit = _sellLimit; + buyLimit = _buyLimit; + dailyLimit = _dailyLimit; + minSell = _minSell; + minBuy = _minBuy; + + emit SetBuyFee(_buyFee); + emit SetSellFee(_sellFee); + emit SetFeeReceiver(_feeReceiver); + emit SetSellLimit(_sellLimit); + emit SetBuyLimit(_buyLimit); + emit SetDailyLimit(_dailyLimit); + emit SetMinSell(_minSell); + emit SetMinBuy(_minBuy); + } + + /** + * @dev sell token to get lisUSD + * @param amount token amount + */ + function sell(uint256 amount) external nonReentrant whenNotPaused { + // check sell limit + checkAndUpdateSellUsed(amount); + + // transfer token from user + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + // calculate fee and real amount + uint256 fee = Math.mulDiv(amount, sellFee, FEE_PRECISION); + uint256 realAmount = amount - fee; + + // mint lisUSD to user + HayLike(lisUSD).mint(msg.sender, realAmount); + + // deposit token to vault manager + IERC20(token).safeIncreaseAllowance(vaultManager, amount); + IVaultManager(vaultManager).deposit(amount); + + // mint fee to fee receiver + if (fee > 0) { + HayLike(lisUSD).mint(feeReceiver, fee); + } + emit Selltoken(msg.sender, realAmount, fee); + } + + /** + * @dev buy token with lisUSD + * @param amount lisUSD amount + */ + function buy(uint256 amount) external nonReentrant whenNotPaused { + // check buy limit + checkAndUpdateBuyUsed(amount); + + // calculate fee and real amount + uint256 fee = Math.mulDiv(amount, buyFee, FEE_PRECISION); + uint256 realAmount = amount - fee; + + // burn lisUSD from user and withdraw token from vault manager + if (realAmount > 0) { + HayLike(lisUSD).burn(msg.sender, realAmount); + IVaultManager(vaultManager).withdraw(msg.sender, realAmount); + } + + // transfer fee to fee receiver + if (fee > 0) { + IERC20(lisUSD).safeTransferFrom(msg.sender, feeReceiver, fee); + } + emit Buytoken(msg.sender, realAmount, fee); + } + + // check sell limit + function checkSellLimit(uint256 amount) public view returns (bool) { + // check min sell amount + if (amount < minSell) { + return false; + } + + // check total sell limit + if (amount + totalSellUsed > sellLimit) { + return false; + } + + return true; + } + + // check and update sell used + function checkAndUpdateSellUsed(uint256 amount) private { + require(checkSellLimit(amount), "exceed sell limit"); + // update total sell used + totalSellUsed += amount; + } + + // check buy limit + function checkBuyLimit(uint256 amount) public view returns (bool) { + // check min buy amount + if (amount < minBuy) { + return false; + } + // check total buy limit + if (amount + totalBuyUsed > buyLimit) { + 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), "exceed buy limit"); + + // update total buy used + if (getDay() != lastBuyDay) { + lastBuyDay = getDay(); + dayBuyUsed = 0; + } + + dayBuyUsed += amount; + totalBuyUsed += 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"); + 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 sell limit + * @param _sellLimit sell limit + */ + function setSellLimit(uint256 _sellLimit) external onlyRole(MANAGER) { + sellLimit = _sellLimit; + emit SetSellLimit(_sellLimit); + } + + /** + * @dev set buy limit + * @param _buyLimit buy limit + */ + function setBuyLimit(uint256 _buyLimit) external onlyRole(MANAGER) { + buyLimit = _buyLimit; + emit SetBuyLimit(_buyLimit); + } + + /** + * @dev set daily limit + * @param _dailyLimit daily limit + */ + function setDailyLimit(uint256 _dailyLimit) external onlyRole(MANAGER) { + 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) { + minBuy = _minBuy; + emit SetMinBuy(_minBuy); + } + + /** + * @dev pause contract + */ + function pause() external onlyRole(PAUSE) { + _pause(); + } + + /** + * @dev toggle pause contract + */ + function togglePause() external onlyRole(DEFAULT_ADMIN_ROLE) { + if (paused()) { + _unpause(); + } else { + _pause(); + } + } +} \ No newline at end of file diff --git a/contracts/psm/VaultManager.sol b/contracts/psm/VaultManager.sol new file mode 100644 index 00000000..4da891ab --- /dev/null +++ b/contracts/psm/VaultManager.sol @@ -0,0 +1,216 @@ +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 "../interfaces/IAdapter.sol"; + +contract VaultManager is AccessControlUpgradeable { + 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 + uint256 public localToken; // local token amount + + uint256 constant public MAX_PRECISION = 10000; + 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 _psm PSM address + * @param _token token address + */ + function initialize( + address _psm, + address _token + ) public initializer { + require(_psm != address(0), "psm cannot be zero address"); + require(_token != address(0), "token cannot be zero address"); + __AccessControl_init(); + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setupRole(MANAGER, msg.sender); + + psm = _psm; + token = _token; + + emit SetPSM(_psm); + emit SetToken(_token); + } + + modifier onlyPSM() { + require(msg.sender == psm, "Only PSM can call this function"); + _; + } + + /** + * @dev deposit token to adapters, only PSM can call this function + * @param amount deposit amount + */ + function deposit(uint256 amount) external onlyPSM { + 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(); + + // deposit token to adapters by adapter point + for (uint256 i = 0; i < adapters.length; i++) { + if (adapters[i].active) { // 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; + } + } + } + + // if remain amount > 0, add to localtoken + if (remain > 0) { + localToken += remain; + } + } + + 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 withdraw token from adapters, only PSM can call this function + * @param receiver receiver address + * @param amount withdraw amount + */ + function withdraw(address receiver, uint256 amount) external onlyPSM { + require(amount > 0, "withdraw amount cannot be zero"); + + uint256 remain = amount; + + // withdraw token from local first + if (localToken >= remain) { + IERC20(token).safeTransfer(receiver, remain); + localToken -= remain; + return; + } else { + IERC20(token).safeTransfer(receiver, localToken); + remain -= localToken; + localToken = 0; + } + + // withdraw token from adapters + for (uint256 i = 0; i < adapters.length; i++) { + uint256 totalAvailableAmount = IAdapter(adapters[i].adapter).totalAvailableAmount(); + if (totalAvailableAmount >= remain) { + IAdapter(adapters[i].adapter).withdraw(receiver, remain); + return; + } else { + IAdapter(adapters[i].adapter).withdraw(receiver, totalAvailableAmount); + remain -= totalAvailableAmount; + } + } + + 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 rebalance token to adapters, only bot can call this function + */ + function rebalance() external onlyRole(BOT) { + uint256 amount = localToken; + for (uint256 i = 0; i < adapters.length; i++) { + amount += IAdapter(adapters[i].adapter).withdrawAll(); + } + + 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).safeTransfer(msg.sender, amount); + + emit EmergencyWithdraw(msg.sender, amount); + } +} diff --git a/contracts/psm/VenusAdapter.sol b/contracts/psm/VenusAdapter.sol new file mode 100644 index 00000000..96d715b8 --- /dev/null +++ b/contracts/psm/VenusAdapter.sol @@ -0,0 +1,167 @@ +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 "../interfaces/IVBep20Delegate.sol"; + +contract VenusAdapter is AccessControlUpgradeable { + using SafeERC20 for IERC20; + address public vaultManager; // vault manager address + address public venusPool; // venus pool address + address public token; // token address + address public vToken; // vToken address + uint256 public quota; // quota + + uint256 public quotaAmount; // quota amount + + bytes32 public constant MANAGER = keccak256("MANAGER"); // manager role + + event Deposit(uint256 amount); + event Withdraw(address account, uint256 amount); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + modifier onlyVaultManager() { + require(msg.sender == vaultManager, "only VaultManager can call this function"); + _; + } + + /** + * @dev initialize contract + * @param _vaultManager vault manager address + * @param _venusPool venus pool address + * @param _token token address + * @param _vToken vToken address + */ + function initialize( + address _vaultManager, + address _venusPool, + address _token, + address _vToken, + uint256 _quotaAmount + ) public initializer { + require(_vaultManager != address(0), "vaultManager cannot be zero address"); + require(_venusPool != address(0), "venusPool cannot be zero address"); + require(_token != address(0), "token cannot be zero address"); + require(_vToken != address(0), "vToken cannot be zero address"); + + __AccessControl_init(); + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setupRole(MANAGER, msg.sender); + + vaultManager = _vaultManager; + token = _token; + venusPool = _venusPool; + vToken = _vToken; + quotaAmount = _quotaAmount; + } + + /** + * @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(venusPool, amount); + + // deposit to venus pool + IVBep20Delegate(venusPool).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"); + + uint256 exchangeRate = IVBep20Delegate(venusPool).exchangeRateStored(); + // calculate vToken amount + uint256 vTokenAmount = Math.mulDiv(amount, 1e18, exchangeRate); + require(IERC20(vToken).balanceOf(address(this)) >= vTokenAmount, "not enough vToken"); + + // withdraw from quota + if (vTokenAmount == 0) { + if (quota < amount) { + _withdrawQuota(); + require(quota >= amount, "not enough quota"); + } + quota -= amount; + IERC20(token).safeTransfer(account, amount); + emit Withdraw(account, amount); + return; + } + // withdraw from venus pool + IERC20(vToken).safeIncreaseAllowance(venusPool, vTokenAmount); + uint256 before = IERC20(token).balanceOf(address(this)); + IVBep20Delegate(venusPool).redeem(vTokenAmount); + uint256 tokenAmount = IERC20(token).balanceOf(address(this)) - before; + + uint256 remain = amount - tokenAmount; + if (remain > 0) { + if (quota < remain) { + _withdrawQuota(); + require(quota >= remain, "not enough quota"); + } + quota -= remain; + } + + // transfer token to account + IERC20(token).safeTransfer(account, amount); + + emit Withdraw(account, tokenAmount); + } + + // withdraw quota from venus pool + function _withdrawQuota() private { + uint256 vTokenAmount = Math.mulDiv(quotaAmount, 1e18, IVBep20Delegate(venusPool).exchangeRateStored()); + require(IERC20(vToken).balanceOf(address(this)) >= vTokenAmount, "not enough vToken"); + IERC20(vToken).safeIncreaseAllowance(venusPool, vTokenAmount); + uint256 before = IERC20(token).balanceOf(address(this)); + IVBep20Delegate(venusPool).redeem(vTokenAmount); + uint256 tokenAmount = IERC20(token).balanceOf(address(this)) - before; + quota += tokenAmount; + } + + /** + * @dev get total available amount + */ + function totalAvailableAmount() public view returns (uint256) { + uint256 vTokenAmount = IERC20(vToken).balanceOf(address(this)); + uint256 tokenAmount = Math.mulDiv(vTokenAmount, IVBep20Delegate(venusPool).exchangeRateStored(), 1e18) + + quota; + return tokenAmount; + } + + /** + * @dev withdraw all token to vault manager + */ + function withdrawAll() external onlyVaultManager returns (uint256) { + uint256 vTokenAmount = IERC20(vToken).balanceOf(address(this)); + uint256 tokenAmount = quota; + if (vTokenAmount > 0) { + uint256 before = IERC20(token).balanceOf(address(this)); + IVBep20Delegate(venusPool).redeem(vTokenAmount); + tokenAmount += IERC20(token).balanceOf(address(this)) - before; + + } + IERC20(token).safeTransfer(vaultManager, tokenAmount); + return tokenAmount; + } + + /** + * @dev set quota amount + * @param _quotaAmount quota amount + */ + function setQuotaAmount(uint256 _quotaAmount) external onlyRole(MANAGER) { + quotaAmount = _quotaAmount; + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 00000000..8ddaf69a --- /dev/null +++ b/foundry.toml @@ -0,0 +1,10 @@ +[profile.default] +src = 'contracts' +out = 'out' +libs = ['node_modules', 'lib'] +test = 'test' +cache_path = 'cache_forge' + +[rpc_endpoints] +bsc-test = "https://data-seed-prebsc-1-s1.binance.org:8545/" +bsc-main = "https://bsc-dataseed.binance.org/" \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 6c835f92..d7e8f1ed 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/lib/openzeppelin-contracts b/lib/openzeppelin-contracts deleted file mode 160000 index 0a25c194..00000000 --- a/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0a25c1940ca220686588c4af3ec526f725fe2582 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable deleted file mode 160000 index 58fa0f81..00000000 --- a/lib/openzeppelin-contracts-upgradeable +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 58fa0f81c4036f1a3b616fdffad2fd27e5d5ce21 diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades deleted file mode 160000 index 372170ba..00000000 --- a/lib/openzeppelin-foundry-upgrades +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 372170ba7deeabe1979cf29ba01a99ddf56dd9e0 diff --git a/package.json b/package.json index ec899435..eb39c6ff 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,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", diff --git a/scripts/dev/psm/deploy_earnPool.js b/scripts/dev/psm/deploy_earnPool.js new file mode 100644 index 00000000..1ac27735 --- /dev/null +++ b/scripts/dev/psm/deploy_earnPool.js @@ -0,0 +1,36 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let lisUSDPool = '0xDA1cA1F0bc8fD75fB51315526403774f4BE25691'; +let lisUSD = '0x785b5d1Bde70bD6042877cA08E4c73e0a40071af'; + +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, [ + 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); + } + +} + +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..9429b076 --- /dev/null +++ b/scripts/dev/psm/deploy_lisUSDPool.js @@ -0,0 +1,38 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let lisUSD = '0x785b5d1Bde70bD6042877cA08E4c73e0a40071af'; +let maxDuty = '1000000034836767751273470154'; // 200% +let vat = "0x382589e4dE7A061fcb9716c203983d8FE847AE0b"; + +async function main() { + const signers = await hre.ethers.getSigners(); + const deployer = signers[0].address; + + const LisUSDPool = await hre.ethers.getContractFactory('LisUSDPool'); + const lisUSDPool = await upgrades.deployProxy(LisUSDPool, [ + lisUSD, + vat, + 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); + } + +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/dev/psm/deploy_listaAdapter.js b/scripts/dev/psm/deploy_listaAdapter.js new file mode 100644 index 00000000..bce764bf --- /dev/null +++ b/scripts/dev/psm/deploy_listaAdapter.js @@ -0,0 +1,33 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let USDC = '0xA528b0E61b72A0191515944cD8818a88d1D1D22b'; +let vaultManager = '0x81Dcf4406f6b6f71637111096514DbfE7DC53e24'; + +async function main() { + const ListaAdapter = await hre.ethers.getContractFactory('ListaAdapter'); + const listaAdapter = await upgrades.deployProxy(ListaAdapter, [ + USDC, + vaultManager + ]); + await listaAdapter.waitForDeployment(); + + const proxyAddress = await listaAdapter.getAddress(); + + console.log('ListaAdapter 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_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..f97fdb26 --- /dev/null +++ b/scripts/dev/psm/deploy_psm.js @@ -0,0 +1,51 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let usdc = '0xA528b0E61b72A0191515944cD8818a88d1D1D22b'; +let lisUSD = '0x785b5d1Bde70bD6042877cA08E4c73e0a40071af'; +let sellFee = 0; +let buyFee = 500; +let sellLimit = '1000000000000000000000000000'; // 1e27 +let buyLimit = '1000000000000000000000000000'; // 1e27 +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, [ + usdc, + deployer, + lisUSD, + sellFee, + buyFee, + sellLimit, + buyLimit, + 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_vaultManager.js b/scripts/dev/psm/deploy_vaultManager.js new file mode 100644 index 00000000..8ea1d414 --- /dev/null +++ b/scripts/dev/psm/deploy_vaultManager.js @@ -0,0 +1,32 @@ +const {ethers, upgrades, run} = require('hardhat') +const hre = require('hardhat') + +let psm = '0xA0a4D7c3282B55Ef88a12AE394f00E9e47487651'; +let usdc = '0xA528b0E61b72A0191515944cD8818a88d1D1D22b'; + +async function main() { + const VaultManager = await hre.ethers.getContractFactory('VaultManager'); + const vaultManager = await upgrades.deployProxy(VaultManager, [ + psm, + usdc + ]); + 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); + } +} + +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..f31da9de 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 = 'ListaAdapter' async function main() { diff --git a/test/psm/EarnPool.t.sol b/test/psm/EarnPool.t.sol new file mode 100644 index 00000000..31b3185c --- /dev/null +++ b/test/psm/EarnPool.t.sol @@ -0,0 +1,162 @@ +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "../../contracts/psm/LisUSDPool.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"; +import "../../contracts/psm/EarnPool.sol"; + +contract EarnPoolTest is Test { + PSM psm; + VaultManager vaultManager; + LisUSDPool lisUSDPool; + EarnPool earnPool; + address admin = address(0x1); + address user1 = address(0x2); + ProxyAdmin proxyAdmin = ProxyAdmin(0xBd8789025E91AF10487455B692419F82523D29Be); + address lisUSD = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; + address USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; + uint256 MAX_DUTY = 1000000005781378656804590540; + uint256 duty = 1000000005781378656804590540; + address vat = 0x33A34eAB3ee892D40420507B820347b1cA2201c4; + + 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(); + + TransparentUpgradeableProxy psmProxy = new TransparentUpgradeableProxy( + address(psmImpl), + address(proxyAdmin), + abi.encodeWithSelector( + psmImpl.initialize.selector, + USDC, + admin, + lisUSD, + 0, + 0, + 1e18 * 1e7, + 1e18*1e7, + 1e18*1e4, + 1e18, + 1e18 + ) + ); + + psm = PSM(address(psmProxy)); + + VaultManager vaultManagerImpl = new VaultManager(); + + TransparentUpgradeableProxy vaultManagerProxy = new TransparentUpgradeableProxy( + address(vaultManagerImpl), + address(proxyAdmin), + abi.encodeWithSelector( + vaultManagerImpl.initialize.selector, + address(psm), + USDC + ) + ); + + vaultManager = VaultManager(address(vaultManagerProxy)); + + psm.setVaultManager(address(vaultManager)); + + LisUSDPool lisUSDPoolImpl = new LisUSDPool(); + TransparentUpgradeableProxy lisUSDPoolProxy = new TransparentUpgradeableProxy( + address(lisUSDPoolImpl), + address(proxyAdmin), + abi.encodeWithSelector( + lisUSDPoolImpl.initialize.selector, + lisUSD, + vat, + MAX_DUTY + ) + ); + + lisUSDPool = LisUSDPool(address(lisUSDPoolProxy)); + + EarnPool earnPoolImpl = new EarnPool(); + TransparentUpgradeableProxy earnPoolProxy = new TransparentUpgradeableProxy( + address(earnPoolImpl), + address(proxyAdmin), + abi.encodeWithSelector( + earnPoolImpl.initialize.selector, + address(lisUSDPool), + lisUSD + ) + ); + earnPool = EarnPool(address(earnPoolProxy)); + + earnPool.setPSM(USDC, address(psm)); + + lisUSDPool.setEarnPool(address(earnPool)); + lisUSDPool.registerPool(USDC, USDC, address(0)); + lisUSDPool.setDuty(duty); + + vm.stopPrank(); + + vm.startPrank(lisUSDAuth); + LisUSD(lisUSD).rely(address(psm)); + LisUSD(lisUSD).rely(address(lisUSDPool)); + vm.stopPrank(); + } + + function test_depositAndWithdraw() public { + deal(USDC, user1, 1000 ether); + deal(lisUSD, user1, 1000 ether); + deal(lisUSD, admin, 10000 ether); + + address[] memory pools = new address[](2); + pools[0] = address(earnPool); + pools[1] = address(lisUSDPool); + + 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); + console.log("user1 USDC 0: ", usdcBalance); + console.log("user1 lisUSD 0: ", lisUSDBalance); + console.log("lisUSDPool lisUSD balance 1: ", IERC20(lisUSD).balanceOf(address(lisUSDPool))); + + skip(1 days); + lisUSDPool.withdraw(pools, 1); + usdcBalance = IERC20(USDC).balanceOf(user1); + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + + uint256 earnPoolBalance = lisUSDPool.poolBalanceOf(address(earnPool), user1); + console.log("user1 earnPool balance 1: ", earnPoolBalance); + console.log("user1 USDC 1: ", usdcBalance); + console.log("user1 lisUSD 1: ", lisUSDBalance); + + lisUSDPool.withdraw(pools, 99 ether); + + usdcBalance = IERC20(USDC).balanceOf(user1); + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + earnPoolBalance = lisUSDPool.poolBalanceOf(address(earnPool), user1); + console.log("user1 earnPool balance 2: ", earnPoolBalance); + console.log("user1 USDC 2: ", usdcBalance); + console.log("user1 lisUSD 2: ", lisUSDBalance); + + vm.stopPrank(); + + } +} \ No newline at end of file diff --git a/test/psm/LisUSDPool.t.sol b/test/psm/LisUSDPool.t.sol new file mode 100644 index 00000000..2d17bcc4 --- /dev/null +++ b/test/psm/LisUSDPool.t.sol @@ -0,0 +1,98 @@ +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "../../contracts/psm/LisUSDPool.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"; +import "../../contracts/psm/EarnPool.sol"; + +contract LisUSDPoolTest is Test { + LisUSDPool lisUSDPool; + address admin = address(0x1); + address user1 = address(0x2); + ProxyAdmin proxyAdmin = ProxyAdmin(0xBd8789025E91AF10487455B692419F82523D29Be); + address lisUSD = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; + address USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; + address vat = 0x33A34eAB3ee892D40420507B820347b1cA2201c4; + + address lisUSDAuth = 0xAca0ed4651ddA1F43f00363643CFa5EBF8774b37; + + uint256 MAX_UINT = 115792089237316195423570985008687907853269984665640564039457584007913129639935; + uint256 MAX_DUTY = 1000000005781378656804590540; + + + function setUp() public { + vm.createSelectFork("bsc-main"); + + vm.deal(admin, 100 ether); + vm.deal(user1, 100 ether); + + vm.startPrank(admin); + LisUSDPool lisUSDPoolImpl = new LisUSDPool(); + TransparentUpgradeableProxy lisUSDPoolProxy = new TransparentUpgradeableProxy( + address(lisUSDPoolImpl), + address(proxyAdmin), + abi.encodeWithSelector( + lisUSDPoolImpl.initialize.selector, + lisUSD, + vat, + MAX_DUTY + ) + ); + + lisUSDPool = LisUSDPool(address(lisUSDPoolProxy)); + + + vm.stopPrank(); + + vm.startPrank(lisUSDAuth); + LisUSD(lisUSD).rely(address(lisUSDPool)); + vm.stopPrank(); + } + + function test_depositAndWithdraw() public { + deal(lisUSD, user1, 100 ether + 1); + deal(lisUSD, admin, 10000 ether); + + address[] memory pools = new address[](1); + pools[0] = address(lisUSDPool); + + 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)); + console.log("user1 lisUSD 0: ", lisUSDBalance); + console.log("pool lisUSD 0: ", poolBalance); + + skip(365 days); + + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + poolBalance = IERC20(lisUSD).balanceOf(address(lisUSDPool)); + console.log("user1 lisUSD 1: ", lisUSDBalance); + console.log("pool lisUSD 1: ", poolBalance); + + skip(365 days); + + uint256 lisUSDPoolBalance = lisUSDPool.balanceOfAssets(user1); + console.log("user1 lisUSDPool balance2: ", lisUSDPoolBalance); + lisUSDPool.withdraw(pools, lisUSDPoolBalance); + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + poolBalance = IERC20(lisUSD).balanceOf(address(lisUSDPool)); + console.log("user1 lisUSD 2: ", lisUSDBalance); + console.log("pool lisUSD 2: ", poolBalance); + + vm.stopPrank(); + + } +} \ No newline at end of file diff --git a/test/psm/ListaAdapter.t.sol b/test/psm/ListaAdapter.t.sol new file mode 100644 index 00000000..2704bacd --- /dev/null +++ b/test/psm/ListaAdapter.t.sol @@ -0,0 +1,124 @@ +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../../contracts/psm/ListaAdapter.sol"; + +contract ListaAdapterTest is Test { + ListaAdapter listaAdapter; + address admin = address(0x1); + address user1 = address(0x2); + address venusPool = 0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8; + address USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; + ProxyAdmin proxyAdmin = ProxyAdmin(0xBd8789025E91AF10487455B692419F82523D29Be); + + function setUp() public { + vm.createSelectFork("bsc-main"); + + vm.deal(admin, 100 ether); + vm.deal(user1, 100 ether); + + vm.startPrank(admin); + ListaAdapter listaAdpaterImpl = new ListaAdapter(); + + TransparentUpgradeableProxy listaAdapterProxy = new TransparentUpgradeableProxy( + address(listaAdpaterImpl), + address(proxyAdmin), + abi.encodeWithSelector( + listaAdpaterImpl.initialize.selector, + USDC, + user1 + ) + ); + + listaAdapter = ListaAdapter(address(listaAdapterProxy)); + + vm.stopPrank(); + + + } + + function test_depositAndWithdraw() public { + deal(USDC, user1, 200 ether); + + uint256 USDCBalance = IERC20(USDC).balanceOf(user1); + console.log("user1 USDC: ", USDCBalance); + + vm.startPrank(user1); + IERC20(USDC).approve(address(listaAdapter), 100 ether); + listaAdapter.deposit(100 ether); + vm.stopPrank(); + + uint256 gemAmount = listaAdapter.totalAvailableAmount(); + console.log("Staked USDC: ", gemAmount); + + + vm.startPrank(user1); + listaAdapter.withdraw(user1, 100 ether); + vm.stopPrank(); + + USDCBalance = IERC20(USDC).balanceOf(user1); + console.log("user1 USDC: ", USDCBalance); + USDCBalance = IERC20(USDC).balanceOf(address(listaAdapter)); + console.log("adapter USDC: ", USDCBalance); + + gemAmount = listaAdapter.totalAvailableAmount(); + console.log("Staked USDC:: ", gemAmount); + + } + + function test_setOperator() public { + vm.startPrank(admin); + listaAdapter.grantRole(listaAdapter.MANAGER(), user1); + vm.stopPrank(); + + bool r = listaAdapter.hasRole(listaAdapter.MANAGER(), user1); + console.log("operator: ", r); + + vm.startPrank(admin); + listaAdapter.revokeRole(listaAdapter.MANAGER(), user1); + vm.stopPrank(); + + r = listaAdapter.hasRole(listaAdapter.MANAGER(), user1); + console.log("operator: ", r); + } + + function test_operatorDepositAndWithdraw() public { + deal(USDC, user1, 200 ether); + + uint256 USDCBalance = IERC20(USDC).balanceOf(user1); + console.log("user1 USDC: ", USDCBalance); + + vm.startPrank(admin); + listaAdapter.grantRole(listaAdapter.MANAGER(), user1); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(USDC).approve(address(listaAdapter), 1000 ether); + listaAdapter.deposit(100 ether); + + uint256 gemAmount = listaAdapter.totalAvailableAmount(); + console.log("Staked USDC: ", gemAmount); + + listaAdapter.withdrawByOperator(10 ether); + + USDCBalance = IERC20(USDC).balanceOf(user1); + console.log("user1 USDC: ", USDCBalance); + USDCBalance = IERC20(USDC).balanceOf(address(listaAdapter)); + console.log("adapter USDC: ", USDCBalance); + gemAmount = listaAdapter.totalAvailableAmount(); + console.log("Staked USDC:: ", gemAmount); + + listaAdapter.depositByOperator(110 ether); + + USDCBalance = IERC20(USDC).balanceOf(user1); + console.log("user1 USDC: ", USDCBalance); + USDCBalance = IERC20(USDC).balanceOf(address(listaAdapter)); + console.log("adapter USDC: ", USDCBalance); + gemAmount = listaAdapter.totalAvailableAmount(); + console.log("Staked USDC:: ", gemAmount); + + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/test/psm/PSM.t.sol b/test/psm/PSM.t.sol new file mode 100644 index 00000000..5404fe05 --- /dev/null +++ b/test/psm/PSM.t.sol @@ -0,0 +1,101 @@ +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "../../contracts/psm/LisUSDPool.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"; +import "../../contracts/hMath.sol"; + +contract PSMTest is Test { + PSM psm; + VaultManager vaultManager; + address admin = address(0x1); + address user1 = address(0x2); + ProxyAdmin proxyAdmin = ProxyAdmin(0xBd8789025E91AF10487455B692419F82523D29Be); + address lisUSD = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; + address USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; + + 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(); + + TransparentUpgradeableProxy psmProxy = new TransparentUpgradeableProxy( + address(psmImpl), + address(proxyAdmin), + abi.encodeWithSelector( + psmImpl.initialize.selector, + USDC, + admin, + lisUSD, + 0, + 500, + 1e18 * 1e7, + 1e18*1e7, + 1e18*1e4, + 1e18, + 1e18 + ) + ); + + psm = PSM(address(psmProxy)); + + VaultManager vaultManagerImpl = new VaultManager(); + + TransparentUpgradeableProxy vaultManagerProxy = new TransparentUpgradeableProxy( + address(vaultManagerImpl), + address(proxyAdmin), + abi.encodeWithSelector( + vaultManagerImpl.initialize.selector, + address(psm), + USDC + ) + ); + + vaultManager = VaultManager(address(vaultManagerProxy)); + + psm.setVaultManager(address(vaultManager)); + + vm.stopPrank(); + + vm.startPrank(lisUSDAuth); + LisUSD(lisUSD).rely(address(psm)); + 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); + console.log("user1 USDC 0: ", usdcBalance / 1 ether); + console.log("user1 lisUSD 0: ", lisUSDBalance / 1 ether); + + psm.buy(100 ether); + + usdcBalance = IERC20(USDC).balanceOf(user1); + lisUSDBalance = IERC20(lisUSD).balanceOf(user1); + console.log("user1 USDC 1: ", usdcBalance / 1 ether); + console.log("user1 lisUSD 1: ", lisUSDBalance / 1 ether); + + vm.stopPrank(); + + } +} \ No newline at end of file diff --git a/test/psm/VaultManager.t.sol b/test/psm/VaultManager.t.sol new file mode 100644 index 00000000..634aa4f8 --- /dev/null +++ b/test/psm/VaultManager.t.sol @@ -0,0 +1,141 @@ +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "../../contracts/psm/LisUSDPool.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/ListaAdapter.sol"; +import "../../contracts/psm/VenusAdapter.sol"; +import "../../contracts/LisUSD.sol"; +import "../../contracts/hMath.sol"; + +contract VaultManagerTest is Test { + VaultManager vaultManager; + ListaAdapter listaAdapter; + VenusAdapter venusAdapter; + address admin = address(0x1); + address user1 = address(0x2); + ProxyAdmin proxyAdmin = ProxyAdmin(0xBd8789025E91AF10487455B692419F82523D29Be); + address lisUSD = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; + address USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; + address venusPool = 0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8; + 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(); + + TransparentUpgradeableProxy vaultManagerProxy = new TransparentUpgradeableProxy( + address(vaultManagerImpl), + address(proxyAdmin), + abi.encodeWithSelector( + vaultManagerImpl.initialize.selector, + address(user1), + USDC + ) + ); + + vaultManager = VaultManager(address(vaultManagerProxy)); + + ListaAdapter listaAdpaterImpl = new ListaAdapter(); + + TransparentUpgradeableProxy listaAdapterProxy = new TransparentUpgradeableProxy( + address(listaAdpaterImpl), + address(proxyAdmin), + abi.encodeWithSelector( + listaAdpaterImpl.initialize.selector, + USDC, + address(vaultManager) + ) + ); + + listaAdapter = ListaAdapter(address(listaAdapterProxy)); + + VenusAdapter venusAdapterImpl = new VenusAdapter(); + + TransparentUpgradeableProxy venusAdapterProxy = new TransparentUpgradeableProxy( + address(venusAdapterImpl), + address(proxyAdmin), + abi.encodeWithSelector( + venusAdapterImpl.initialize.selector, + address(vaultManager), + venusPool, + USDC, + vUSDC, + quotaAmount + ) + ); + + venusAdapter = VenusAdapter(address(venusAdapterProxy)); + + vm.stopPrank(); + } + + function test_depositAndWithdraw() public { + deal(USDC, user1, 1000 ether); + + vm.startPrank(user1); + IERC20(USDC).approve(address(vaultManager), MAX_UINT); + + vaultManager.deposit(100 ether); + + uint256 usdcBalance = IERC20(USDC).balanceOf(user1); + uint256 vaultManagerUSDC = IERC20(USDC).balanceOf(address(vaultManager)); + console.log("user1 USDC 0: ", usdcBalance / 1 ether); + console.log("vaultManager USDC 0: ", vaultManagerUSDC / 1 ether); + + vaultManager.withdraw(user1, 100 ether); + + usdcBalance = IERC20(USDC).balanceOf(user1); + vaultManagerUSDC = IERC20(USDC).balanceOf(address(vaultManager)); + console.log("user1 USDC 1: ", usdcBalance / 1 ether); + console.log("vaultManager USDC 1: ", vaultManagerUSDC / 1 ether); + + vm.stopPrank(); + + } + + function test_addAdapter() public { + deal(USDC, user1, 1000 ether); + + vm.startPrank(admin); + vaultManager.addAdapter(address(listaAdapter), 1000); + vaultManager.addAdapter(address(venusAdapter), 2000); + vm.stopPrank(); + + vm.startPrank(user1); + IERC20(USDC).approve(address(vaultManager), MAX_UINT); + + vaultManager.deposit(1000 ether); + + uint256 listaAdapterBalance = listaAdapter.totalAvailableAmount(); + uint256 venusAdapterBalance = venusAdapter.totalAvailableAmount(); + uint256 vaultManagerBalance = vaultManager.localToken(); + console.log("listaAdapterBalance 0: ", listaAdapterBalance); + console.log("venusAdapterBalance 0: ", venusAdapterBalance); + console.log("vaultManagerBalance 0: ", vaultManagerBalance); + + vaultManager.withdraw(user1, 900 ether); + listaAdapterBalance = listaAdapter.totalAvailableAmount(); + venusAdapterBalance = venusAdapter.totalAvailableAmount(); + vaultManagerBalance = vaultManager.localToken(); + console.log("listaAdapterBalance 1: ", listaAdapterBalance); + console.log("venusAdapterBalance 1: ", venusAdapterBalance); + console.log("vaultManagerBalance 1: ", vaultManagerBalance); + + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/test/psm/VenusAdapter.t.sol b/test/psm/VenusAdapter.t.sol new file mode 100644 index 00000000..91f7b5fd --- /dev/null +++ b/test/psm/VenusAdapter.t.sol @@ -0,0 +1,85 @@ +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(0x1); + address user1 = address(0x2); + address venusPool = 0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8; + 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(); + + TransparentUpgradeableProxy venusAdapterProxy = new TransparentUpgradeableProxy( + address(venusAdapterImpl), + address(proxyAdmin), + abi.encodeWithSelector( + venusAdapterImpl.initialize.selector, + user1, + venusPool, + USDC, + vUSDC, + quotaAmount + ) + ); + + venusAdapter = VenusAdapter(address(venusAdapterProxy)); + + vm.stopPrank(); + + + } + + function test_depositAndWithdraw() public { + deal(USDC, user1, 200 ether); + + uint256 vUSDCBalance = IERC20(vUSDC).balanceOf(address(venusAdapter)); + console.log("vUSDC: ", vUSDCBalance); + + vm.startPrank(user1); + IERC20(USDC).approve(address(venusAdapter), 100 ether); + venusAdapter.deposit(100 ether); + vm.stopPrank(); + + vUSDCBalance = IERC20(vUSDC).balanceOf(address(venusAdapter)); + console.log("vUSDC: ", vUSDCBalance); + + uint256 gemAmount = venusAdapter.totalAvailableAmount(); + console.log("Staked USDC:: ", gemAmount); + + + vm.startPrank(user1); + venusAdapter.withdraw(user1, 99 ether); + vm.stopPrank(); + + uint256 USDCBalance = IERC20(USDC).balanceOf(user1); + console.log("user1 USDC: ", USDCBalance); + USDCBalance = IERC20(USDC).balanceOf(address(venusAdapter)); + console.log("adapter USDC: ", USDCBalance); + vUSDCBalance = IERC20(vUSDC).balanceOf(address(venusAdapter)); + console.log("vUSDC: ", vUSDCBalance); + +// 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); + + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d451756b..5af59279 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"