diff --git a/contracts/Controller.sol b/contracts/Controller.sol index ac4f9ba..39557a4 100644 --- a/contracts/Controller.sol +++ b/contracts/Controller.sol @@ -35,6 +35,9 @@ contract Controller is ERC20, Ownable, ReentrancyGuard { uint public depositLimit; uint public userDepositLimit; + uint public depositShareThreshold; + uint public withdrawShareThreshold; + event NewStrategy(address oldStrategy, address newStrategy); event NewTreasury(address oldTreasury, address newTreasury); event NewDepositLimit(uint oldLimit, uint newLimit); @@ -56,6 +59,10 @@ contract Controller is ERC20, Ownable, ReentrancyGuard { want = _want; archimedes = _archimedes; treasury = _treasury; + + // 99% of share/token value + depositShareThreshold = (10 ** _want.decimals()) * 9900 / RATIO_PRECISION; + withdrawShareThreshold = (10 ** _want.decimals()) * 9900 / RATIO_PRECISION; } function decimals() override public view returns (uint8) { @@ -90,6 +97,18 @@ contract Controller is ERC20, Ownable, ReentrancyGuard { return pid; } + function setDepositShareThreshold(uint _t) external onlyOwner nonReentrant { + require(depositShareThreshold != _t, "Same value"); + + depositShareThreshold = _t; + } + + function setWithdrawShareThreshold(uint _t) external onlyOwner nonReentrant { + require(withdrawShareThreshold != _t, "Same value"); + + withdrawShareThreshold = _t; + } + function setTreasury(address _treasury) external onlyOwner nonReentrant { require(_treasury != treasury, "Same address"); require(_treasury != address(0), "!ZeroAddress"); @@ -145,6 +164,8 @@ contract Controller is ERC20, Ownable, ReentrancyGuard { function deposit(address _senderUser, uint _amount) external onlyArchimedes nonReentrant { require(!_strategyPaused(), "Strategy paused"); require(_amount > 0, "Insufficient amount"); + // Ensure pool is healthy at deposit + require(currentSharePrice() >= depositShareThreshold, "Low Share Price"); _checkDepositLimit(_senderUser, _amount); IStrategy(strategy).beforeMovement(); @@ -169,11 +190,16 @@ contract Controller is ERC20, Ownable, ReentrancyGuard { _mint(_senderUser, shares); _strategyDeposit(); + + // Ensure the final step still is healthy + require(currentSharePrice() >= depositShareThreshold, "Low Share Price"); } // Withdraw partial funds, normally used with a vault withdrawal function withdraw(address _senderUser, uint _shares) external onlyArchimedes nonReentrant returns (uint) { require(_shares > 0, "Insufficient shares"); + // Ensure pool is healthy before withdraw + require(currentSharePrice() >= withdrawShareThreshold, "Low Share Price"); IStrategy(strategy).beforeMovement(); // This line has to be calc before burn @@ -208,6 +234,9 @@ contract Controller is ERC20, Ownable, ReentrancyGuard { if (!_strategyPaused()) { _strategyDeposit(); } + // Ensure pool still healthy + require(currentSharePrice() >= withdrawShareThreshold, "Low Share Price"); + return withdrawn; } @@ -277,4 +306,11 @@ contract Controller is ERC20, Ownable, ReentrancyGuard { require(_amount <= availableUserDeposit(_user), "Max userDepositLimit reached"); } } + + function currentSharePrice() public view returns (uint) { + uint _totalSupply = totalSupply(); + uint _precision = 10 ** decimals(); + + return _totalSupply <= 0 ? _precision : (balance() * _precision / _totalSupply); + } } diff --git a/test/integration/ControllerMStableStrat-test.js b/test/integration/ControllerMStableStrat-test.js index 3b54d56..d858474 100644 --- a/test/integration/ControllerMStableStrat-test.js +++ b/test/integration/ControllerMStableStrat-test.js @@ -511,4 +511,62 @@ describe('Controller mStable Strat with DAI', () => { expect(await DAI.balanceOf(strat.address)).to.be.equal(0) expect(await REWARD_TOKEN.balanceOf(strat.address)).to.be.equal(0) }) + + describe('setDepositShareThreshold', async () => { + it('should be reverted for non admin', async () => { + await expect(controller.connect(bob).setDepositShareThreshold(10)).to.be.revertedWith( + 'Ownable: caller is not the owner' + ) + }) + + it('should change depositShareThreshold', async () => { + expect(await controller.depositShareThreshold()).to.be.equal(0.99e18 + '') + await controller.setDepositShareThreshold(10) + expect(await controller.depositShareThreshold()).to.be.equal(10) + }) + + it('should revert for low depositShareThreshold', async () => { + const newBalance = ethers.BigNumber.from('' + 1e18).mul(100000) // 100000 DAI + await setCustomBalanceFor(DAI.address, bob.address, newBalance) + + await controller.setDepositShareThreshold(1.1e18 + '') // just to prove it + + await DAI.connect(bob).approve(archimedes.address, newBalance) + await expect(archimedes.connect(bob).depositAll(0, zeroAddress)).to.be.revertedWith('Low Share Price') + }) + + it('should revert for low depositShareThreshold after deposit in strat', async () => { + const newBalance = ethers.BigNumber.from('' + 1e18).mul(100000) // 100000 DAI + await setCustomBalanceFor(DAI.address, bob.address, newBalance) + + await controller.setDepositShareThreshold(1.0e18 + '') // just to prove it + + await DAI.connect(bob).approve(archimedes.address, newBalance) + await expect(archimedes.connect(bob).depositAll(0, zeroAddress)).to.be.revertedWith('Low Share Price') + }) + }) + + describe('setWithdrawShareThreshold', async () => { + it('should be reverted for non admin', async () => { + await expect(controller.connect(bob).setWithdrawShareThreshold(10)).to.be.revertedWith( + 'Ownable: caller is not the owner' + ) + }) + + it('should change withdrawShareThreshold', async () => { + expect(await controller.withdrawShareThreshold()).to.be.equal(0.99e18 + '') + await controller.setWithdrawShareThreshold(10) + expect(await controller.withdrawShareThreshold()).to.be.equal(10) + }) + + it('should revert for low withdrawShareThreshold', async () => { + const newBalance = ethers.BigNumber.from('' + 1e18).mul(100000) // 100000 DAI + await setCustomBalanceFor(DAI.address, bob.address, newBalance) + await controller.setWithdrawShareThreshold(1.1e18 + '') // just to prove it + + await DAI.connect(bob).approve(archimedes.address, newBalance) + await waitFor(archimedes.connect(bob).depositAll(0, zeroAddress)) + await expect(archimedes.connect(bob).withdrawAll(0)).to.be.revertedWith('Low Share Price') + }) + }) }) diff --git a/utils b/utils index db6562b..4dc8bb2 160000 --- a/utils +++ b/utils @@ -1 +1 @@ -Subproject commit db6562b5e0c8caabe5b0f54e65bbb85636f1b46a +Subproject commit 4dc8bb27a78d291838496d826c91a472e439487b