diff --git a/smart-contracts/contracts/StrollManager.sol b/smart-contracts/contracts/StrollManager.sol index 5a03fcc..971291f 100644 --- a/smart-contracts/contracts/StrollManager.sol +++ b/smart-contracts/contracts/StrollManager.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "./interfaces/IERC20Mod.sol"; import "./interfaces/IStrollManager.sol"; + // solhint-disable not-rely-on-time /// @title StrollManager /// @author Harsh Prakash <0xharsh@proton.me> @@ -29,6 +30,9 @@ contract StrollManager is IStrollManager, Ownable { uint64 _minLower, uint64 _minUpper ) { + if (_icfa == address(0)) revert ZeroAddress(); + if (_minLower >= _minUpper) revert WrongLimits(_minLower, _minUpper); + CFA_V1 = IConstantFlowAgreementV1(_icfa); minLower = _minLower; minUpper = _minUpper; @@ -149,6 +153,20 @@ contract StrollManager is IStrollManager, Ownable { } } + /// @dev IStrollManager.setLimits implementation. + function setLimits(uint64 _lowerLimit, uint64 _upperLimit) + external + onlyOwner + { + if (_lowerLimit >= _upperLimit) + revert WrongLimits(_lowerLimit, _upperLimit); + + minLower = _lowerLimit; + minUpper = _upperLimit; + + emit LimitsChanged(_lowerLimit, _upperLimit); + } + /// @dev IStrollManager.getTopUp implementation. function getTopUp( address _user, @@ -194,7 +212,7 @@ contract StrollManager is IStrollManager, Ownable { TopUp storage topUp = topUps[_index]; address user = topUp.user; - + if (user != msg.sender && topUp.expiry >= block.timestamp) revert UnauthorizedCaller(msg.sender, user); @@ -244,8 +262,12 @@ contract StrollManager is IStrollManager, Ownable { uint256 superBalance = topUp.superToken.balanceOf(topUp.user); uint256 positiveFlowRate = uint256(uint96(-1 * flowRate)); - if (superBalance <= (positiveFlowRate * topUp.lowerLimit)) { - return positiveFlowRate * topUp.upperLimit; + // Selecting max between user defined limits and global limits. + uint64 maxLowerLimit = (topUp.lowerLimit < minLower)? minLower: topUp.lowerLimit; + uint64 maxUpperLimit = (topUp.upperLimit < minUpper)? minUpper: topUp.upperLimit; + + if (superBalance <= (positiveFlowRate * maxLowerLimit)) { + return positiveFlowRate * maxUpperLimit; } } diff --git a/smart-contracts/contracts/interfaces/IStrollManager.sol b/smart-contracts/contracts/interfaces/IStrollManager.sol index 3e0af21..3ce4553 100644 --- a/smart-contracts/contracts/interfaces/IStrollManager.sol +++ b/smart-contracts/contracts/interfaces/IStrollManager.sol @@ -15,10 +15,17 @@ interface IStrollManager { uint256 lowerLimit, uint256 upperLimit ); - event TopUpDeleted(bytes32 indexed id, address indexed user, address indexed superToken, address strategy, address liquidityToken); + event TopUpDeleted( + bytes32 indexed id, + address indexed user, + address indexed superToken, + address strategy, + address liquidityToken + ); event PerformedTopUp(bytes32 indexed id, uint256 topUpAmount); event AddedApprovedStrategy(address indexed strategy); event RemovedApprovedStrategy(address indexed strategy); + event LimitsChanged(uint64 lowerLimit, uint64 upperLimit); /// Custom error to indicate that null address has been passed. error ZeroAddress(); @@ -50,6 +57,11 @@ interface IStrollManager { /// @param minLimit Minimum limit (upper/lower) expected. error InsufficientLimits(uint64 limitGiven, uint64 minLimit); + /// Custom error to indicate that the limits are wrong (lower limit >= upper limit). + /// @param lowerLimit Limit (upper/lower) given by the user. + /// @param upperLimit Minimum limit (upper/lower) expected. + error WrongLimits(uint64 lowerLimit, uint64 upperLimit); + /** * @notice Struct representing a top-up. * @param user Address of the user who created the top-up. @@ -82,6 +94,15 @@ interface IStrollManager { */ function removeApprovedStrategy(address _strategy) external; + /** + * @notice Sets the global limits for top-ups. + * @param _lowerLimit Triggers top up if stream can't be continued for this amount of seconds. + * @param _upperLimit Increase supertoken balance to continue stream for this amount of seconds. + * @dev If the previous top-ups don't adhere to the current global limits, the global limits will be enforced. + * i.e., max(global limit, user defined limit) is always taken. + */ + function setLimits(uint64 _lowerLimit, uint64 _upperLimit) external; + /** * @notice Creates a new top up task. * @param _superToken The supertoken to monitor/top up. @@ -118,7 +139,10 @@ interface IStrollManager { * @param _index Index of top up. * @return The top up. */ - function getTopUpByIndex(bytes32 _index) external view returns (TopUp memory); + function getTopUpByIndex(bytes32 _index) + external + view + returns (TopUp memory); /** * @notice Gets a top up by index. @@ -138,7 +162,10 @@ interface IStrollManager { * @param _index Index of top up. * @return _amount The amount of supertoken to top up. */ - function checkTopUpByIndex(bytes32 _index) external view returns (uint256 _amount); + function checkTopUpByIndex(bytes32 _index) + external + view + returns (uint256 _amount); /** * @notice Checks if a top up is required. diff --git a/smart-contracts/test/StrollManager.test.js b/smart-contracts/test/StrollManager.test.js index 31fa732..2ec7ddf 100644 --- a/smart-contracts/test/StrollManager.test.js +++ b/smart-contracts/test/StrollManager.test.js @@ -3,14 +3,14 @@ /* eslint-disable no-undef */ const { parseUnits } = require("@ethersproject/units"); -const { expect } = require("chai"); +const { expect, assert } = require("chai"); const zeroAddress = "0x0000000000000000000000000000000000000000"; const helper = require("./../helpers/helpers"); const devEnv = require("./utils/setEnv"); -const MIN_LOWER = 2; -const MIN_UPPER = 7; +const MIN_LOWER = helper.getSeconds(2); +const MIN_UPPER = helper.getSeconds(5); let accounts, owner, user, streamReceiver; let env; @@ -370,16 +370,24 @@ describe("#3 - StrollManager: Register TopUps", function () { strategy.address, dai.address, expiry + 1000, - 20, - 20 + helper.getSeconds(20), + helper.getSeconds(20) ); const topUp = await strollManager.getTopUp( user.address, daix.address, dai.address ); - assert.equal(topUp.lowerLimit, 20, "wrong lowerLimit on update"); - assert.equal(topUp.upperLimit, 20, "wrong upperLimit on update"); + assert.equal( + topUp.lowerLimit, + helper.getSeconds(20), + "wrong lowerLimit on update" + ); + assert.equal( + topUp.upperLimit, + helper.getSeconds(20), + "wrong upperLimit on update" + ); assert.equal(topUp.expiry, expiry + 1000, "wrong expiry on update"); }); }); @@ -799,4 +807,68 @@ describe("#6 - perform Top Up", function () { strollManager.performTopUp(user.address, daix.address, dai.address) ).to.be.revertedWith(`TopUpNotRequired("${result}")`); }); + it("Case #6.4 - TopUp using max values (global limits)", async () => { + const flowRate = parseUnits("300", 18).div( + helper.getBigNumber(helper.getSeconds(30)) + ); + await strollManager + .connect(user) + .createTopUp( + daix.address, + strategy.address, + dai.address, + helper.getTimeStampNow() + helper.getSeconds(365), + helper.getSeconds(5), + helper.getSeconds(5) + ); + + let tx = await strollManager.setLimits( + helper.getSeconds(6), + helper.getSeconds(8) + ); + const limitsChangedEvent = await helper.getEvents(tx, "LimitsChanged"); + assert.equal( + limitsChangedEvent[0].args.lowerLimit, + helper.getSeconds(6), + "wrong lower limit" + ); + assert.equal( + limitsChangedEvent[0].args.upperLimit, + helper.getSeconds(8), + "wrong upper limit" + ); + + // approve superToken + await dai.connect(user).approve(daix.address, parseUnits("10000", 18)); + // approve strategy + await dai.connect(user).approve(strategy.address, parseUnits("10000", 18)); + // get some superToken + await daix.connect(user).upgrade(parseUnits("20", 18)); + await createStream(daix, user, streamReceiver, flowRate.toString(), "0x"); + + let balance = await daix.balanceOf(user.address); + console.log("Balance before increase time: ", balance.toString()); + + await helper.increaseTime(3600 * 24 * 5); + + balance = await daix.balanceOf(user.address); + console.log("Balance after increase time: ", balance.toString()); + + tx = await strollManager.performTopUp( + user.address, + daix.address, + dai.address + ); + + const TopUpEvent = await helper.getEvents(tx, "PerformedTopUp"); + const after = await daix.balanceOf(user.address); + + console.log("After: ", after.toString()); + + expect(after.sub(balance)).to.be.closeTo( + TopUpEvent[0].args.topUpAmount, + parseUnits("0.01", 18) + ); + assert.isAbove(after, balance, "balance should go up"); + }); });