diff --git a/contracts/callManagers/LendingManager/LendingLogicAave.sol b/contracts/callManagers/LendingManager/LendingLogicAave.sol index 3b3f59d..76a8d46 100644 --- a/contracts/callManagers/LendingManager/LendingLogicAave.sol +++ b/contracts/callManagers/LendingManager/LendingLogicAave.sol @@ -13,6 +13,7 @@ contract LendingLogicAave is ILendingLogic { uint16 public referralCode; constructor(address _lendingPool, uint16 _referralCode) { + require(_lendingPool != address(0), "LENDING_POOL_INVALID"); lendingPool = IAaveLendingPool(_lendingPool); referralCode = _referralCode; } diff --git a/contracts/callManagers/LendingManager/LendingLogicCompound.sol b/contracts/callManagers/LendingManager/LendingLogicCompound.sol index 23cc3df..16bd48d 100644 --- a/contracts/callManagers/LendingManager/LendingLogicCompound.sol +++ b/contracts/callManagers/LendingManager/LendingLogicCompound.sol @@ -13,6 +13,7 @@ contract LendingLogicCompound is ILendingLogic { bytes32 public constant PROTOCOL = keccak256(abi.encodePacked("Compound")); constructor(address _lendingRegistry) { + require(_lendingRegistry != address(0), "INVALID_LENDING_REGISTRY"); lendingRegistry = LendingRegistry(_lendingRegistry); } diff --git a/contracts/callManagers/LendingManager/LendingManager.sol b/contracts/callManagers/LendingManager/LendingManager.sol index 77cd132..84331ae 100644 --- a/contracts/callManagers/LendingManager/LendingManager.sol +++ b/contracts/callManagers/LendingManager/LendingManager.sol @@ -3,11 +3,12 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.7.1; import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/math/Math.sol"; import "./LendingRegistry.sol"; import "../../interfaces/IExperiPie.sol"; -contract LendingManager is Ownable { +contract LendingManager is Ownable, ReentrancyGuard { using Math for uint256; LendingRegistry public lendingRegistry; @@ -21,6 +22,8 @@ contract LendingManager is Ownable { @param _basket Address of the pool/pie/basket to manage */ constructor(address _lendingRegistry, address _basket) public { + require(_lendingRegistry != address(0), "INVALID_LENDING_REGISTRY"); + require(_basket != address(0), "INVALID_BASKET"); lendingRegistry = LendingRegistry(_lendingRegistry); basket = IExperiPie(_basket); } @@ -31,7 +34,7 @@ contract LendingManager is Ownable { @param _amount Amount of underlying to lend @param _protocol Bytes32 protocol key to lend to */ - function lend(address _underlying, uint256 _amount, bytes32 _protocol) public onlyOwner { + function lend(address _underlying, uint256 _amount, bytes32 _protocol) public onlyOwner nonReentrant { // _amount or actual balance, whatever is less uint256 amount = _amount.min(IERC20(_underlying).balanceOf(address(basket))); @@ -57,7 +60,7 @@ contract LendingManager is Ownable { @param _wrapped Address of the wrapped token @param _amount Amount of the wrapped token to unlend */ - function unlend(address _wrapped, uint256 _amount) public onlyOwner { + function unlend(address _wrapped, uint256 _amount) public onlyOwner nonReentrant { // unlend token // _amount or actual balance, whatever is less uint256 amount = _amount.min(IERC20(_wrapped).balanceOf(address(basket))); @@ -83,6 +86,7 @@ contract LendingManager is Ownable { @param _wrapped Address of the wrapped token to bounce to another protocol @param _amount Amount of the wrapped token to bounce to the other protocol @param _toProtocol Protocol to deposit bounced tokens in + @dev Uses reentrency protection of unlend() and lend() */ function bounce(address _wrapped, uint256 _amount, bytes32 _toProtocol) external { unlend(_wrapped, _amount); diff --git a/contracts/callManagers/RSIManager.sol b/contracts/callManagers/RSIManager.sol index 48e7c11..02f62a9 100644 --- a/contracts/callManagers/RSIManager.sol +++ b/contracts/callManagers/RSIManager.sol @@ -14,6 +14,11 @@ contract RSISynthetixManager { bytes32 public immutable assetShortKey; bytes32 public immutable assetLongKey; + // Value under which to go long (30 * 10**18 == 30) + int256 public immutable rsiBottom; + // Value under which to go short + int256 public immutable rsiTop; + IPriceReferenceFeed public immutable priceFeed; IExperiPie public immutable basket; ISynthetix public immutable synthetix; @@ -33,6 +38,8 @@ contract RSISynthetixManager { address _assetLong, bytes32 _assetShortKey, bytes32 _assetLongKey, + int256 _rsiBottom, + int256 _rsiTop, address _priceFeed, address _basket, address _synthetix @@ -41,6 +48,23 @@ contract RSISynthetixManager { assetLong = _assetLong; assetShortKey = _assetShortKey; assetLongKey = _assetLongKey; + + require(_assetShort != address(0), "INVALID_ASSET_SHORT"); + require(_assetLong != address(0), "INVALID_ASSET_LONG"); + require(_assetShortKey != bytes32(0), "INVALID_ASSET_SHORT_KEY"); + require(_assetLongKey != bytes32(0), "INVALID_ASSET_LONG_KEY"); + + require(_rsiBottom < _rsiTop, "RSI bottom should be bigger than RSI top"); + require(_rsiBottom > 0, "RSI bottom should be bigger than 0"); + require(_rsiTop < 100 * 10**18, "RSI top should be less than 100"); + + require(_priceFeed != address(0), "INVALID_PRICE_FEED"); + require(_basket != address(0), "INVALID_BASKET"); + require(_synthetix != address(0), "INVALID_SYNTHETIX"); + + rsiBottom = _rsiBottom; + rsiTop = _rsiTop; + priceFeed = IPriceReferenceFeed(_priceFeed); basket = IExperiPie(_basket); synthetix = ISynthetix(_synthetix); @@ -51,11 +75,11 @@ contract RSISynthetixManager { RoundData memory roundData = readLatestRound(); require(roundData.updatedAt > 0, "Round not complete"); - if(roundData.answer <= 30 * 10**18) { + if(roundData.answer <= rsiBottom) { // long long(); return; - } else if(roundData.answer >= 70 * 10**18) { + } else if(roundData.answer >= rsiTop) { // Short short(); return; diff --git a/contracts/facets/Basket/BasketFacet.sol b/contracts/facets/Basket/BasketFacet.sol index 7f178fc..35e2e64 100644 --- a/contracts/facets/Basket/BasketFacet.sol +++ b/contracts/facets/Basket/BasketFacet.sol @@ -18,12 +18,15 @@ contract BasketFacet is ReentryProtection, CallProtection, IBasketFacet { uint256 public constant MAX_ENTRY_FEE = 10**17; // 10% uint256 public constant MAX_EXIT_FEE = 10**17; // 10% uint256 public constant MAX_ANNUAL_FEE = 10**17; // 10% + uint256 public constant HUNDRED_PERCENT = 10 ** 18; - + // Assuming a block gas limit of 12M this allows for a gas consumption per token of roughly 333k allowing 2M of overhead for addtional operations + uint256 public constant MAX_TOKENS = 30; function addToken(address _token) external override protectedCall { LibBasketStorage.BasketStorage storage bs = LibBasketStorage.basketStorage(); require(!bs.inPool[_token], "TOKEN_ALREADY_IN_POOL"); + require(bs.tokens.length < MAX_TOKENS, "TOKEN_LIMIT_REACHED"); // Enforce minimum to avoid rounding errors; (Minimum value is the same as in Balancer) require(balance(_token) >= MIN_AMOUNT, "BALANCE_TOO_SMALL"); @@ -41,17 +44,14 @@ contract BasketFacet is ReentryProtection, CallProtection, IBasketFacet { bs.inPool[_token] = false; // remove token from array - // TODO consider limiting max amount of tokens to mitigate running out of gas. for(uint256 i; i < bs.tokens.length; i ++) { if(address(bs.tokens[i]) == _token) { bs.tokens[i] = bs.tokens[bs.tokens.length - 1]; bs.tokens.pop(); - + emit TokenRemoved(_token); break; } } - - emit TokenRemoved(_token); } function setEntryFee(uint256 _fee) external override protectedCall { @@ -75,6 +75,7 @@ contract BasketFacet is ReentryProtection, CallProtection, IBasketFacet { } function setAnnualizedFee(uint256 _fee) external override protectedCall { + chargeOutstandingAnnualizedFee(); require(_fee <= MAX_ANNUAL_FEE, "FEE_TOO_BIG"); LibBasketStorage.basketStorage().annualizedFee = _fee; emit AnnualizedFeeSet(_fee); @@ -85,6 +86,7 @@ contract BasketFacet is ReentryProtection, CallProtection, IBasketFacet { } function setFeeBeneficiary(address _beneficiary) external override protectedCall { + chargeOutstandingAnnualizedFee(); LibBasketStorage.basketStorage().feeBeneficiary = _beneficiary; emit FeeBeneficiarySet(_beneficiary); } @@ -94,7 +96,7 @@ contract BasketFacet is ReentryProtection, CallProtection, IBasketFacet { } function setEntryFeeBeneficiaryShare(uint256 _share) external override protectedCall { - require(_share <= 10**18, "FEE_SHARE_TOO_BIG"); + require(_share <= HUNDRED_PERCENT, "FEE_SHARE_TOO_BIG"); LibBasketStorage.basketStorage().entryFeeBeneficiaryShare = _share; emit EntryFeeBeneficiaryShareSet(_share); } @@ -104,7 +106,7 @@ contract BasketFacet is ReentryProtection, CallProtection, IBasketFacet { } function setExitFeeBeneficiaryShare(uint256 _share) external override protectedCall { - require(_share <= 10**18, "FEE_SHARE_TOO_BIG"); + require(_share <= HUNDRED_PERCENT, "FEE_SHARE_TOO_BIG"); LibBasketStorage.basketStorage().exitFeeBeneficiaryShare = _share; emit ExitFeeBeneficiaryShareSet(_share); } @@ -119,7 +121,7 @@ contract BasketFacet is ReentryProtection, CallProtection, IBasketFacet { chargeOutstandingAnnualizedFee(); LibBasketStorage.BasketStorage storage bs = LibBasketStorage.basketStorage(); uint256 totalSupply = LibERC20Storage.erc20Storage().totalSupply; - require(totalSupply.add(_amount) < this.getCap(), "MAX_POOL_CAP_REACHED"); + require(totalSupply.add(_amount) <= this.getCap(), "MAX_POOL_CAP_REACHED"); uint256 feeAmount = _amount.mul(bs.entryFee).div(10**18); diff --git a/contracts/facets/Call/CallFacet.sol b/contracts/facets/Call/CallFacet.sol index 04a0e43..db02b06 100644 --- a/contracts/facets/Call/CallFacet.sol +++ b/contracts/facets/Call/CallFacet.sol @@ -10,6 +10,8 @@ import "./LibCallStorage.sol"; contract CallFacet is ReentryProtection, ICallFacet { + uint256 public constant MAX_CALLERS = 50; + // uses modified call protection modifier to also allow whitelisted addresses to call modifier protectedCall() { require( @@ -20,11 +22,17 @@ contract CallFacet is ReentryProtection, ICallFacet { _; } - function addCaller(address _caller) external override { + modifier onlyOwner() { require(msg.sender == LibDiamond.diamondStorage().contractOwner, "NOT_ALLOWED"); + _; + } + + function addCaller(address _caller) external override onlyOwner { LibCallStorage.CallStorage storage callStorage = LibCallStorage.callStorage(); + require(callStorage.callers.length < MAX_CALLERS, "TOO_MANY_CALLERS"); require(!callStorage.canCall[_caller], "IS_ALREADY_CALLER"); + require(_caller != address(0), "INVALID_CALLER"); callStorage.callers.push(_caller); callStorage.canCall[_caller] = true; @@ -32,8 +40,7 @@ contract CallFacet is ReentryProtection, ICallFacet { emit CallerAdded(_caller); } - function removeCaller(address _caller) external override { - require(msg.sender == LibDiamond.diamondStorage().contractOwner, "NOT_ALLOWED"); + function removeCaller(address _caller) external override onlyOwner { LibCallStorage.CallStorage storage callStorage = LibCallStorage.callStorage(); require(callStorage.canCall[_caller], "IS_NOT_CALLER"); @@ -96,9 +103,10 @@ contract CallFacet is ReentryProtection, ICallFacet { bytes memory _calldata, uint256 _value ) internal { + require(address(this).balance >= _value, "ETH_BALANCE_TOO_LOW"); (bool success, ) = _target.call{ value: _value }(_calldata); require(success, "CALL_FAILED"); - emit Call(_target, _calldata, _value); + emit Call(msg.sender, _target, _calldata, _value); } function canCall(address _caller) external view override returns (bool) { diff --git a/contracts/facets/ERC20/ERC20Facet.sol b/contracts/facets/ERC20/ERC20Facet.sol index 15d8edf..3d87834 100644 --- a/contracts/facets/ERC20/ERC20Facet.sol +++ b/contracts/facets/ERC20/ERC20Facet.sol @@ -21,6 +21,18 @@ contract ERC20Facet is IERC20, IERC20Facet, CallProtection { LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); LibERC20Storage.ERC20Storage storage es = LibERC20Storage.erc20Storage(); + require( + bytes(es.name).length == 0 && + bytes(es.symbol).length == 0, + "ALREADY_INITIALIZED" + ); + + require( + bytes(_name).length != 0 && + bytes(_symbol).length != 0, + "INVALID_PARAMS" + ); + require(msg.sender == ds.contractOwner, "Must own the contract."); LibERC20.mint(msg.sender, _initialSupply); @@ -54,11 +66,33 @@ contract ERC20Facet is IERC20, IERC20Facet, CallProtection { override returns (bool) { + require(_spender != address(0), "SPENDER_INVALID"); LibERC20Storage.erc20Storage().allowances[msg.sender][_spender] = _amount; emit Approval(msg.sender, _spender, _amount); return true; } + function increaseApproval(address _spender, uint256 _amount) external override returns (bool) { + require(_spender != address(0), "SPENDER_INVALID"); + LibERC20Storage.ERC20Storage storage es = LibERC20Storage.erc20Storage(); + es.allowances[msg.sender][_spender] = es.allowances[msg.sender][_spender].add(_amount); + emit Approval(msg.sender, _spender, es.allowances[msg.sender][_spender]); + return true; + } + + function decreaseApproval(address _spender, uint256 _amount) external override returns (bool) { + require(_spender != address(0), "SPENDER_INVALID"); + LibERC20Storage.ERC20Storage storage es = LibERC20Storage.erc20Storage(); + uint256 oldValue = es.allowances[msg.sender][_spender]; + if (_amount > oldValue) { + es.allowances[msg.sender][_spender] = 0; + } else { + es.allowances[msg.sender][_spender] = oldValue.sub(_amount); + } + emit Approval(msg.sender, _spender, es.allowances[msg.sender][_spender]); + return true; + } + function transfer(address _to, uint256 _amount) external override @@ -74,6 +108,7 @@ contract ERC20Facet is IERC20, IERC20Facet, CallProtection { uint256 _amount ) external override returns (bool) { LibERC20Storage.ERC20Storage storage es = LibERC20Storage.erc20Storage(); + require(_from != address(0), "FROM_INVALID"); // Update approval if not set to max uint256 if (es.allowances[_from][msg.sender] != uint256(-1)) { @@ -83,6 +118,7 @@ contract ERC20Facet is IERC20, IERC20Facet, CallProtection { } _transfer(_from, _to, _amount); + return true; } function allowance(address _owner, address _spender) @@ -107,9 +143,6 @@ contract ERC20Facet is IERC20, IERC20Facet, CallProtection { address _to, uint256 _amount ) internal { - if (_to == address(0)) { - return LibERC20.burn(msg.sender, _amount); - } LibERC20Storage.ERC20Storage storage es = LibERC20Storage.erc20Storage(); es.balances[_from] = es.balances[_from].sub(_amount); diff --git a/contracts/facets/ERC20/LibERC20.sol b/contracts/facets/ERC20/LibERC20.sol index 5583fb0..02f3786 100644 --- a/contracts/facets/ERC20/LibERC20.sol +++ b/contracts/facets/ERC20/LibERC20.sol @@ -12,6 +12,8 @@ library LibERC20 { event Transfer(address indexed from, address indexed to, uint256 amount); function mint(address _to, uint256 _amount) internal { + require(_to != address(0), "INVALID_TO_ADDRESS"); + LibERC20Storage.ERC20Storage storage es = LibERC20Storage.erc20Storage(); es.balances[_to] = es.balances[_to].add(_amount); diff --git a/contracts/factories/PieFactoryContract.sol b/contracts/factories/PieFactoryContract.sol index 00f7a92..5d86763 100644 --- a/contracts/factories/PieFactoryContract.sol +++ b/contracts/factories/PieFactoryContract.sol @@ -37,6 +37,7 @@ contract PieFactoryContract is Ownable { } function removeFacet(uint256 _index) external onlyOwner { + require(_index < defaultCut.length, "INVALID_INDEX"); emit FacetRemoved(defaultCut[_index]); defaultCut[_index] = defaultCut[defaultCut.length - 1]; defaultCut.pop(); diff --git a/contracts/interfaces/ICallFacet.sol b/contracts/interfaces/ICallFacet.sol index ff17586..c084669 100644 --- a/contracts/interfaces/ICallFacet.sol +++ b/contracts/interfaces/ICallFacet.sol @@ -6,7 +6,7 @@ interface ICallFacet { event CallerAdded(address indexed caller); event CallerRemoved(address indexed caller); - event Call(address indexed target, bytes data, uint256 value); + event Call(address indexed caller, address indexed target, bytes data, uint256 value); /** @notice Lets whitelisted callers execute a batch of arbitrary calls from the pool. Reverts if one of the calls fails diff --git a/contracts/interfaces/IERC20Facet.sol b/contracts/interfaces/IERC20Facet.sol index d586a17..6f58c9b 100644 --- a/contracts/interfaces/IERC20Facet.sol +++ b/contracts/interfaces/IERC20Facet.sol @@ -47,4 +47,18 @@ interface IERC20Facet { string memory _name, string memory _symbol ) external; + + /** + @notice Increase the amount of tokens another address can spend + @param _spender Spender + @param _amount Amount to increase by + */ + function increaseApproval(address _spender, uint256 _amount) external returns (bool); + + /** + @notice Decrease the amount of tokens another address can spend + @param _spender Spender + @param _amount Amount to decrease by + */ + function decreaseApproval(address _spender, uint256 _amount) external returns (bool); } \ No newline at end of file diff --git a/test/LendingManager.ts b/test/LendingManager.ts index e56e65a..b8016fa 100644 --- a/test/LendingManager.ts +++ b/test/LendingManager.ts @@ -176,6 +176,17 @@ describe("LendingManager", function() { await timeTraveler.revertSnapshot(); }); + describe("Constructor", async() => { + it("Should fail when lending registry is set to zero address", async() => { + const promise = deployContract(signers[0], LendingManagerArtifact, [constants.AddressZero, pie.address]); + await expect(promise).to.be.revertedWith("INVALID_LENDING_REGISTRY"); + }); + it("Should fail when basket is set to zero address", async() => { + const promise = deployContract(signers[0], LendingManagerArtifact, [lendingRegistry.address, constants.AddressZero]); + await expect(promise).to.be.revertedWith("INVALID_BASKET"); + }); + }); + describe("Lending", async() => { describe("Aave", async() => { it("Lending less than max should work", async() => { diff --git a/test/basket.ts b/test/basket.ts index 2dfc096..ccbfbdd 100644 --- a/test/basket.ts +++ b/test/basket.ts @@ -6,7 +6,7 @@ import { Signer, constants, BigNumber, utils, Contract, BytesLike } from "ethers import BasketFacetArtifact from "../artifacts/BasketFacet.json"; import Erc20FacetArtifact from "../artifacts/ERC20Facet.json"; import MockTokenArtifact from "../artifacts/MockToken.json"; -import { Erc20Facet, BasketFacet, DiamondFactoryContract, MockToken } from "../typechain"; +import { Erc20Facet, BasketFacet, DiamondFactoryContract, MockToken, MockTokenFactory } from "../typechain"; import {IExperiPieFactory} from "../typechain/IExperiPieFactory"; import {IExperiPie} from "../typechain/IExperiPie"; import TimeTraveler from "../utils/TimeTraveler"; @@ -514,6 +514,21 @@ describe("BasketFacet", function() { it("Adding a token which is already in the pool should fail", async() => { await expect(experiPie.addToken(testTokens[0].address)).to.be.revertedWith("TOKEN_ALREADY_IN_POOL"); }); + it("Adding more than max tokens should fail", async() => { + const tokens = await experiPie.getTokens(); + for(let i = 0; i < 30 - tokens.length; i++) { + const token = await (deployContract(signers[0], MockTokenArtifact, ["Mock", "Mock"])) as MockToken; + await token.mint(parseEther("1000000"), account); + await token.transfer(experiPie.address, parseEther("1")); + await experiPie.addToken(token.address); + } + + const token = await (deployContract(signers[0], MockTokenArtifact, ["Mock", "Mock"])) as MockToken; + await token.mint(parseEther("1000000"), account); + await token.transfer(experiPie.address, parseEther("1")); + + await expect(experiPie.addToken(token.address)).to.revertedWith("TOKEN_LIMIT_REACHED"); + }); it("Removing a token", async() => { const tokensBefore = await experiPie.getTokens(); await experiPie.removeToken(testTokens[1].address); @@ -565,9 +580,16 @@ describe("BasketFacet", function() { // zero because no beneficiary expect(await experiPie.calcOutStandingAnnualizedFee()).to.be.eq(0) - await experiPie.setFeeBeneficiary(await signers[1].getAddress()) - // only fee for 1 year - expect(await experiPie.calcOutStandingAnnualizedFee()).to.be.eq(parseEther("2")) + + const tx2 = await experiPie.setFeeBeneficiary(await signers[1].getAddress()) + + timestamp = (await ethers.provider.getBlock(tx2.blockNumber)).timestamp; + await ethers.provider.send("evm_setNextBlockTimestamp", [timestamp + 60*60*24*365]); + await ethers.provider.send("evm_mine", []); + + // only fee for 1 year (small rounding error due to block time being off) + expect(await experiPie.calcOutStandingAnnualizedFee()).to.be.gt(parseEther("2")) + expect(await experiPie.calcOutStandingAnnualizedFee()).to.be.lt(parseEther("2.00000000127")); }) it("outStandingAnnualizedFee, 2 years", async() => { // year 1 @@ -577,12 +599,15 @@ describe("BasketFacet", function() { await ethers.provider.send("evm_setNextBlockTimestamp", [timestamp + 60*60*24*365]); tx = await experiPie.chargeOutstandingAnnualizedFee() timestamp = (await ethers.provider.getBlock(tx.blockNumber)).timestamp; - expect(await experiPie.balanceOf(await signers[1].getAddress())).to.be.eq(balanceY1) + expect(await experiPie.balanceOf(await signers[1].getAddress())).to.be.gt(balanceY1) + expect(await experiPie.balanceOf(await signers[1].getAddress())).to.be.lt(balanceY1.add("64687975647")) // year 2 (compounding, original balance + new fee + fee on previous fee) const balanceY2 = balanceY1.add(parseEther("2")).add(parseEther("2").div(100).mul(2)); await ethers.provider.send("evm_setNextBlockTimestamp", [timestamp + 60*60*24*365]); await experiPie.chargeOutstandingAnnualizedFee() - expect(await experiPie.balanceOf(await signers[1].getAddress())).to.be.eq(balanceY2) + // * small discrepency due to inacurate block time + expect(await experiPie.balanceOf(await signers[1].getAddress())).to.be.gt(balanceY2) + expect(await experiPie.balanceOf(await signers[1].getAddress())).to.be.lte(balanceY2.add("65981735159")); }) }) describe("Fee setters", async () => { diff --git a/test/call.ts b/test/call.ts index 723a860..0c37539 100644 --- a/test/call.ts +++ b/test/call.ts @@ -90,18 +90,20 @@ describe("CallFacet", function() { describe("Call test", async () => { it("Test lock call", async () => { - const latestBlock = await ethers.provider.getBlockNumber(); + const lockBlock = (await ethers.provider.getBlockNumber()) + 100; - const call = await experiPie.populateTransaction.setLock(latestBlock - 1); + const call = await experiPie.populateTransaction.setLock(lockBlock); await experiPie.call( [call.to], [call.data], [0] ); - + + const lockBlockValue = await experiPie.getLockBlock(); const lock = await experiPie.getLock(); - expect(lock).to.be.false; + expect(lockBlockValue).to.eq(lockBlock); + expect(lock).to.be.true; }); it("Send contract ether", async () => { let ether = await ethers.provider.getBalance(experiPie.address); @@ -127,6 +129,19 @@ describe("CallFacet", function() { const difference = userBalanceAfter.sub(userBalanceBefore); expect(difference).to.eq(parseEther("9")); }); + it("Sending ether while not having enough balance should throw an error", async() => { + let ether = await ethers.provider.getBalance(experiPie.address); + expect(ether).to.eq("0"); + + await signers[0].sendTransaction({to: experiPie.address, value: parseEther("10")}); + + ether = await ethers.provider.getBalance(experiPie.address); + expect(ether).to.eq(parseEther("10")); + + const user = await signers[4].getAddress(); + + await expect(experiPie.call([user], ["0x00"], [parseEther("10.1")])).to.be.revertedWith("ETH_BALANCE_TOO_LOW"); + }); it("Send contract erc20 token", async () => { let balance = await testTokens[0].balanceOf(experiPie.address); expect(balance).to.eq(0); @@ -223,8 +238,9 @@ describe("CallFacet", function() { describe("Adding and removal of callers", async() => { - const PLACE_HOLDER_1 = "0x0000000000000000000000000000000000000001" - const PLACE_HOLDER_2 = "0x0000000000000000000000000000000000000002" + const PLACE_HOLDER_1 = "0x0000000000000000000000000000000000000001"; + const PLACE_HOLDER_2 = "0x0000000000000000000000000000000000000002"; + const PLACE_HOLDER_3 = "0x000000000000000000000000000000000000aaaa"; it("Adding a caller should work", async() => { await experiPie.addCaller(PLACE_HOLDER_1); @@ -253,6 +269,17 @@ describe("CallFacet", function() { it("Adding a caller from a non owner should fail", async() => { await expect(experiPie.connect(signers[1]).addCaller(PLACE_HOLDER_1)).to.be.revertedWith("NOT_ALLOWED"); }); + it("Adding more than 50 callers should fail", async() => { + for(let i = 0; i < 50; i ++) { + const address = utils.hexZeroPad([i + 1], 20); + await experiPie.addCaller(address); + } + + await expect(experiPie.addCaller(PLACE_HOLDER_3)).to.be.revertedWith("TOO_MANY_CALLERS"); + }); + it("Adding the zero address as caller should fail", async() => { + await expect(experiPie.addCaller(constants.AddressZero)).to.be.revertedWith("INVALID_CALLER"); + }); it("Removing a caller should work", async() => { await experiPie.addCaller(PLACE_HOLDER_1); await experiPie.removeCaller(PLACE_HOLDER_1); diff --git a/test/erc20.ts b/test/erc20.ts index 3f72604..25b84c5 100644 --- a/test/erc20.ts +++ b/test/erc20.ts @@ -9,6 +9,7 @@ import { Erc20Facet, BasketFacet, DiamondFactoryContract } from "../typechain"; import {IExperiPieFactory} from "../typechain/IExperiPieFactory"; import {IExperiPie} from "../typechain/IExperiPie"; import TimeTraveler from "../utils/TimeTraveler"; +import { parseEther } from "ethers/lib/utils"; chai.use(solidity); @@ -181,6 +182,64 @@ describe("ERC20Facet", function() { await experiPie.approve(account2, constants.WeiPerEther); await experiPie.approve(account2, 0); }); + it("Approving the zero address should fail", async() => { + await expect(experiPie.approve(constants.AddressZero, parseEther("1"))).to.be.revertedWith("SPENDER_INVALID"); + }); + }); + describe("increaseApproval", async() => { + it("Should emit event", async () => { + await expect(experiPie.increaseApproval(account2, constants.WeiPerEther)) + .to.emit(experiPie, "Approval") + .withArgs(account, account2, constants.WeiPerEther); + }); + it("Should work when there was no approved amount before", async () => { + await experiPie.increaseApproval(account2, constants.WeiPerEther); + const approvalAmount = await experiPie.allowance(account, account2); + expect(approvalAmount).to.eq(constants.WeiPerEther); + }); + it("Should work when there was an approved amount before", async () => { + await experiPie.increaseApproval(account2, constants.WeiPerEther); + await experiPie.increaseApproval(account2, constants.WeiPerEther); + const approvalAmount = await experiPie.allowance(account, account2); + expect(approvalAmount).to.eq(constants.WeiPerEther.mul(2)); + }); + it("Increasing approval beyond max uint256 should fail", async () => { + await experiPie.increaseApproval(account2, constants.MaxUint256); + await expect(experiPie.increaseApproval(account2, constants.WeiPerEther)).to.be.revertedWith( + "SafeMath: addition overflow" + ); + }); + it("Increasing approval to the zero address should fail", async() => { + await expect(experiPie.increaseApproval(constants.AddressZero, parseEther("10"))).to.be.revertedWith("SPENDER_INVALID"); + }); + }); + describe("decreaseApproval", async() => { + beforeEach(async () => { + await experiPie.approve(account2, constants.WeiPerEther); + }); + it("Should emit event", async () => { + await expect(experiPie.decreaseApproval(account2, constants.WeiPerEther)) + .to.emit(experiPie, "Approval") + .withArgs(account, account2, constants.Zero); + }); + it("Decreasing part of the approval should work", async () => { + await experiPie.decreaseApproval(account2, constants.WeiPerEther.div(2)); + const approvalAmount = await experiPie.allowance(account, account2); + expect(approvalAmount).to.eq(constants.WeiPerEther.div(2)); + }); + it("Decreasing the entire approval should work", async () => { + await experiPie.decreaseApproval(account2, constants.WeiPerEther); + const approvalAmount = await experiPie.allowance(account, account2); + expect(approvalAmount).to.eq(constants.Zero); + }); + it("Decreasing more than the approval amount should set approval to zero", async () => { + await experiPie.decreaseApproval(account2, constants.WeiPerEther.mul(2)); + const approvalAmount = await experiPie.allowance(account, account2); + expect(approvalAmount).to.eq(constants.Zero); + }); + it("Decreasing approval to the zero address should fail", async() => { + await expect(experiPie.decreaseApproval(constants.AddressZero, parseEther("1"))).to.be.revertedWith("SPENDER_INVALID"); + }); }); describe("transferFrom", async () => { beforeEach(async () => { @@ -222,5 +281,50 @@ describe("ERC20Facet", function() { const approvalAmount = await experiPie.allowance(account, account2); expect(approvalAmount).to.eq(constants.MaxUint256); }); + it("Should fail when _from is zero address", async() => { + await expect(experiPie.transferFrom(constants.AddressZero, account, parseEther("1"))).to.be.revertedWith("FROM_INVALID"); + }); + }); + describe("mint", async() => { + it("Minting from a non owner should fail", async() => { + await expect(experiPie.connect(signers[1]).mint(account2, parseEther("1"))).to.be.revertedWith("NOT_ALLOWED"); + }); + it("Minting to the zero address should fail", async() => { + await expect(experiPie.mint(constants.AddressZero, parseEther("1"))).to.be.revertedWith("INVALID_TO_ADDRESS"); + }); + it("Minting tokens should work", async() => { + const amount = parseEther("1"); + + const totalSupplyBefore = await experiPie.totalSupply(); + const balanceBefore = await experiPie.balanceOf(account2); + + await experiPie.mint(account2, amount); + + const totalSupplyAfter = await experiPie.totalSupply(); + const balanceAfter = await experiPie.balanceOf(account2); + + expect(balanceAfter).to.eq(balanceBefore.add(amount)); + expect(totalSupplyAfter).eq(totalSupplyBefore.add(amount)); + }); + }); + describe("burn", async() => { + beforeEach(async() => { + await experiPie.mint(account2, parseEther("1000")); + }); + it("Burning tokens from a non owner address should fail", async() => { + await expect(experiPie.connect(account2).burn(account2, parseEther("1"))).to.be.revertedWith("NOT_ALLOWED"); + }); + it("Burning tokens should work", async() => { + const amount = parseEther("1"); + + const balanceBefore = await experiPie.balanceOf(account2); + const totalSupplyBefore = await experiPie.totalSupply(); + await experiPie.burn(account2, amount); + const balanceAfter = await experiPie.balanceOf(account2); + const totalSupplyAfter = await experiPie.totalSupply(); + + expect(balanceAfter).to.eq(balanceBefore.sub(amount)); + expect(totalSupplyAfter).to.eq(totalSupplyBefore.sub(amount)) + }); }); }) \ No newline at end of file diff --git a/test/lendingLogicAave.ts b/test/lendingLogicAave.ts index beb72a3..9458db8 100644 --- a/test/lendingLogicAave.ts +++ b/test/lendingLogicAave.ts @@ -53,6 +53,11 @@ describe("LendingLogicAave", function() { await timeTraveler.revertSnapshot(); }); + it("Deploying with lendingPool set to the zero address should fail", async() => { + const promise = deployContract(signers[0], LendingLogicAaveArtifact, [constants.AddressZero, 0]); + await expect(promise).to.be.revertedWith("LENDING_POOL_INVALID"); + }); + it("lend()", async() => { const calls = await lendingLogic.lend(token.address, mintAmount); diff --git a/test/lendingLogicCompound.ts b/test/lendingLogicCompound.ts index 2870140..1af866c 100644 --- a/test/lendingLogicCompound.ts +++ b/test/lendingLogicCompound.ts @@ -62,6 +62,11 @@ describe("LendingLogicCompound", function() { await timeTraveler.revertSnapshot(); }); + it("Deploying with the lending registry to the zero address should fail", async() => { + const promise = deployContract(signers[0], LendingLogicCompoundArtifact, [constants.AddressZero]); + await expect(promise).to.be.revertedWith("INVALID_LENDING_REGISTRY"); + }); + it("lend()", async() => { const calls = await lendingLogic.lend(token.address, mintAmount); diff --git a/test/rsiManager.ts b/test/rsiManager.ts index eebcd8b..f1d14b9 100644 --- a/test/rsiManager.ts +++ b/test/rsiManager.ts @@ -161,6 +161,8 @@ describe("RSIManager", function() { longToken.address, shortTokenKey, longTokenKey, + parseEther("30"), + parseEther("70"), priceFeed.address, pie.address, synthetix.address @@ -175,6 +177,167 @@ describe("RSIManager", function() { await timeTraveler.revertSnapshot(); }); + describe("constructor", async() => { + it("Deploying with an invalid assetShort should fail", async() => { + const promise = deployContract(signers[0], RSISynthetixManagerArtifact, [ + constants.AddressZero, + longToken.address, + shortTokenKey, + longTokenKey, + parseEther("30"), + parseEther("70"), + priceFeed.address, + pie.address, + synthetix.address + ]); + + await expect(promise).to.be.revertedWith("INVALID_ASSET_SHORT"); + }); + it("Deploying with an invalid assetLong should fail", async() => { + const promise = deployContract(signers[0], RSISynthetixManagerArtifact, [ + shortToken.address, + constants.AddressZero, + shortTokenKey, + longTokenKey, + parseEther("30"), + parseEther("70"), + priceFeed.address, + pie.address, + synthetix.address + ]); + + await expect(promise).to.be.revertedWith("INVALID_ASSET_LONG"); + }); + + it("Deploying with an invalid assetShortKey should fail", async() => { + const promise = deployContract(signers[0], RSISynthetixManagerArtifact, [ + shortToken.address, + longToken.address, + "0x0000000000000000000000000000000000000000000000000000000000000000", + longTokenKey, + parseEther("30"), + parseEther("70"), + priceFeed.address, + pie.address, + synthetix.address + ]); + + await expect(promise).to.be.revertedWith("INVALID_ASSET_SHORT_KEY"); + }); + + it("Deploying with an invalid assetLongKey should fail", async() => { + const promise = deployContract(signers[0], RSISynthetixManagerArtifact, [ + shortToken.address, + longToken.address, + shortTokenKey, + "0x0000000000000000000000000000000000000000000000000000000000000000", + parseEther("30"), + parseEther("70"), + priceFeed.address, + pie.address, + synthetix.address + ]); + + await expect(promise).to.be.revertedWith("INVALID_ASSET_LONG_KEY"); + }); + + it("Deploying with a RSI bottom bigger than RSI top should fail", async() => { + const promise = deployContract(signers[0], RSISynthetixManagerArtifact, [ + shortToken.address, + longToken.address, + shortTokenKey, + longTokenKey, + parseEther("30"), + parseEther("29"), + priceFeed.address, + pie.address, + synthetix.address + ]); + + await expect(promise).to.be.revertedWith("RSI bottom should be bigger than RSI top"); + }); + + it("Deploying with a RSI bottom below zero should fail", async() => { + const promise = deployContract(signers[0], RSISynthetixManagerArtifact, [ + shortToken.address, + longToken.address, + shortTokenKey, + longTokenKey, + -1, + parseEther("29"), + priceFeed.address, + pie.address, + synthetix.address + ]); + + await expect(promise).to.be.revertedWith("RSI bottom should be bigger than 0"); + }); + + it("Deploying with a RSI top above 100 should should fail", async() => { + const promise = deployContract(signers[0], RSISynthetixManagerArtifact, [ + shortToken.address, + longToken.address, + shortTokenKey, + longTokenKey, + parseEther("10"), + parseEther("101"), + priceFeed.address, + pie.address, + synthetix.address + ]); + + await expect(promise).to.be.revertedWith("RSI top should be less than 100"); + }); + + it("Deploying with an invalid pricefeed should fail", async() => { + const promise = deployContract(signers[0], RSISynthetixManagerArtifact, [ + shortToken.address, + longToken.address, + shortTokenKey, + longTokenKey, + parseEther("30"), + parseEther("70"), + constants.AddressZero, + pie.address, + synthetix.address + ]); + + await expect(promise).to.be.revertedWith("INVALID_PRICE_FEED"); + }); + + it("Deploying with an invalid basket should fail", async() => { + const promise = deployContract(signers[0], RSISynthetixManagerArtifact, [ + shortToken.address, + longToken.address, + shortTokenKey, + longTokenKey, + parseEther("30"), + parseEther("70"), + priceFeed.address, + constants.AddressZero, + synthetix.address + ]); + + await expect(promise).to.be.revertedWith("INVALID_BASKET"); + }); + + it("Deploying with an invalid synthetix address should fail", async() => { + const promise = deployContract(signers[0], RSISynthetixManagerArtifact, [ + shortToken.address, + longToken.address, + shortTokenKey, + longTokenKey, + parseEther("30"), + parseEther("70"), + priceFeed.address, + pie.address, + constants.AddressZero + ]); + + await expect(promise).to.be.revertedWith("INVALID_SYNTHETIX"); + }); + }); + describe("long", async() => { it("Switching from short to long", async() => { await priceFeed.update(parseEther("10"));