diff --git a/contracts/camVaultsLeverage/camDaiLeverage.sol b/contracts/camVaultsLeverage/camDaiLeverage.sol new file mode 100644 index 0000000..bddd044 --- /dev/null +++ b/contracts/camVaultsLeverage/camDaiLeverage.sol @@ -0,0 +1,220 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import "../interfaces/IAAVE.sol"; +import "../interfaces/ICamDAI.sol"; +import "../interfaces/IVault.sol"; +import "../interfaces/IQuickswapV2Router02.sol"; +import "../interfaces/IUniswapV2Callee.sol"; +import "../interfaces/IUniswapV2Pair.sol"; +import "openzeppelin-4/token/ERC20/IERC20.sol"; +import "openzeppelin-4/access/Ownable.sol"; + +contract LeverageFactory { + + mapping(address => address[]) public index; + + function createNew() external returns (address) { + camDaiLeverage _camDaiLeverage = new camDaiLeverage(msg.sender); + address contractAddress = address(_camDaiLeverage); + index[msg.sender].push(contractAddress); + return contractAddress; + } + + function getContractAddresses(address account) external view returns (address[] memory) { + return index[account]; + } + +} + +contract camDaiLeverage is Ownable, IUniswapV2Callee { + + IERC20 private DAI = IERC20(0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063); + IERC20 private amDAI = IERC20(0x27F8D03b3a2196956ED754baDc28D73be8830A6e); + IERC20 private MAI = IERC20(0xa3Fa99A148fA48D14Ed51d610c367C61876997F1); + IERC20 private USDC = IERC20(0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174); + ICamDAI private camDAI = ICamDAI(0xE6C23289Ba5A9F0Ef31b8EB36241D5c800889b7b); + + IAAVE private AAVE = IAAVE(0x8dFf5E27EA6b7AC08EbFdf9eB090F32ee9a30fcf); + IVault private vault = IVault(0xD2FE44055b5C874feE029119f70336447c8e8827); + IQuickswapV2Router02 private QuickswapV2Router02 = IQuickswapV2Router02(0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff); + IUniswapV2Factory private QuickswapFactory = IUniswapV2Factory(0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32); + + uint public vaultID; + uint constant private max = type(uint256).max; + + constructor (address _newOwner) { + //Create vault + vaultID = vault.createVault(); + //Approves + DAI.approve(address(AAVE), max); + amDAI.approve(address(camDAI), max); + camDAI.approve(address(vault), max); + MAI.approve(address(vault), max); + MAI.approve(address(QuickswapV2Router02), max); + DAI.approve(address(QuickswapV2Router02), max); + //Transfer ownership + transferOwnership(_newOwner); + } + + function _triggerFlash(IERC20 tokenA, IERC20 tokenB, uint amountA, uint amountB, uint _type) internal { + address pair = QuickswapFactory.getPair(address(tokenA), address(tokenB)); + require(pair != address(0), "zeroAddress"); + address token0 = IUniswapV2Pair(pair).token0(); + address token1 = IUniswapV2Pair(pair).token1(); + + uint amount0Out = address(tokenA) == token0 ? amountA : amountB; + uint amount1Out = address(tokenA) == token1 ? amountA : amountB; + + // need to pass some data to trigger uniswapV2Call + bytes memory data = abi.encode(token0, token1, _type); + + IUniswapV2Pair(pair).swap(amount0Out, amount1Out, address(this), data); + } + + function _calculateFlashLoanFee(uint flashAmount) internal pure returns (uint, uint) { + if (flashAmount == 0) { + return (0, 0); + } + // about 0.3% + uint fee = ((flashAmount * 3) / 997) + 1; + uint amountToRepay = flashAmount + fee; + return (amountToRepay, fee); // (flashAmount + fee, fee) + } + + function _calculateOnePercent(uint amount) internal pure returns (uint) { + //Return 1% of amount + uint _100 = 100e18; + uint _1 = 1e18; + return ((amount * _1) / _100); + } + + function _getAmountsIn(uint amountOut, IERC20 _in, IERC20 _out) internal view returns (uint) { + //Calculate how much `_in` do you need for receiving an specified amount of `_out` + address[] memory path = new address[](2); + path[0] = address(_in); + path[1] = address(_out); + + uint256[] memory amountsIn = QuickswapV2Router02.getAmountsIn( + amountOut, + path + ); + + return amountsIn[0]; + } + + function _swap(IERC20 _in, IERC20 _out) internal { + //swap in -> out + uint256 _inBalance = _in.balanceOf(address(this)); + + if (_inBalance != 0) { + address[] memory path = new address[](2); + path[0] = address(_in); + path[1] = address(_out); + + uint256[] memory amountsOut = QuickswapV2Router02.getAmountsOut( + _inBalance, + path + ); + + uint256 minAmount = amountsOut[1] - _calculateOnePercent(amountsOut[1]); // 1% slippage + address receiver = address(this); + + QuickswapV2Router02.swapExactTokensForTokens( + _inBalance, + minAmount, + path, + receiver, + block.timestamp + ); + } + + } + + function _doRuloInternal(uint feeA) internal { + address thisContract = address(this); + uint totalCollateral = DAI.balanceOf(thisContract); + AAVE.deposit(address(DAI), totalCollateral, thisContract, 0); + camDAI.enter(amDAI.balanceOf(thisContract)); + uint toDeposit = camDAI.balanceOf(thisContract); + vault.depositCollateral(vaultID, toDeposit); + uint toBorrow = (totalCollateral / (vault._minimumCollateralPercentage() + 10)) * 100; //10% secure + uint enoughAmount = _getAmountsIn(toBorrow + feeA, MAI, DAI); //Borrow enough MAI to not be short + vault.borrowToken(vaultID, enoughAmount + _calculateOnePercent(enoughAmount)); //Borrow 1% cause slippage o the next line + _swap(MAI, DAI); + } + + function _undoRuloInternal() internal { + address thisContract = address(this); + + uint debt = getVaultDebt(); + if (debt != 0) { + vault.payBackToken(vaultID, debt); + vault.withdrawCollateral(vaultID, getVaultCollateral()); + camDAI.leave(camDAI.balanceOf(thisContract)); + AAVE.withdraw(address(DAI), max, thisContract); + _swap(DAI, MAI); + } + } + + function doRulo(uint amount) external onlyOwner { + DAI.transferFrom(msg.sender, address(this), amount); + uint optimalAmount = (amount * 100) / ((vault._minimumCollateralPercentage() + 10) - 100); //Optimal amount formula: see calc.py + require(optimalAmount <= vault.getDebtCeiling(), "!debtCeiling"); + _triggerFlash(DAI, USDC, optimalAmount, 0, 0); + } + + function undoRulo() external onlyOwner { + require(getVaultDebt() > 0, "there is no rulo to undo"); + _triggerFlash(MAI, USDC, getVaultDebt(), 0, 1); + //Close position, send DAI to owner + _swap(MAI, DAI); // !! can be optimized + DAI.transfer(owner(), DAI.balanceOf(address(this))); + MAI.transfer(owner(), MAI.balanceOf(address(this))); + } + + function transferTokens(address _tokenAddress) external onlyOwner { + //Used for "rescue" tokens + IERC20(_tokenAddress).transfer(owner(), IERC20(_tokenAddress).balanceOf(address(this))); + } + + function getVaultCollateral() public view returns (uint256) { + return vault.vaultCollateral(vaultID); + } + + function getVaultDebt() public view returns (uint256) { + return vault.vaultDebt(vaultID); + } + + function getCollateralPercentage() public view returns (uint256) { + return vault.checkCollateralPercentage(vaultID); + } + + //Uniswap callback + function uniswapV2Call(address _sender, uint _amount0, uint _amount1, bytes calldata _data) external override { + address token0 = IUniswapV2Pair(msg.sender).token0(); + address token1 = IUniswapV2Pair(msg.sender).token1(); + address pair = QuickswapFactory.getPair(token0, token1); + require(msg.sender == pair, "!pair"); + require(_sender == address(this), "!sender"); + + (address tokenA, address tokenB, uint _type) = abi.decode(_data, (address, address, uint)); + + (uint amountToRepayA, uint feeA) = _calculateFlashLoanFee(_amount0); + (uint amountToRepayB, uint feeB) = _calculateFlashLoanFee(_amount1); + + uint fee = feeB == 0 ? feeA : feeB; + + //0 -> _doRuloInternal + //1 -> _undoRuloInternal + if (_type == 0) { + _doRuloInternal(fee); + }else{ + _undoRuloInternal(); + } + + IERC20(tokenA).transfer(pair, amountToRepayA); + IERC20(tokenB).transfer(pair, amountToRepayB); + } + +} \ No newline at end of file diff --git a/contracts/interfaces/IAAVE.sol b/contracts/interfaces/IAAVE.sol new file mode 100644 index 0000000..9e4d020 --- /dev/null +++ b/contracts/interfaces/IAAVE.sol @@ -0,0 +1,17 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +interface IAAVE { + function deposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external; + + function withdraw( + address asset, + uint256 amount, + address to + ) external returns (uint256); +} \ No newline at end of file diff --git a/contracts/interfaces/ICamDAI.sol b/contracts/interfaces/ICamDAI.sol new file mode 100644 index 0000000..b496ad2 --- /dev/null +++ b/contracts/interfaces/ICamDAI.sol @@ -0,0 +1,10 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +interface ICamDAI { + function approve(address spender, uint256 amount) external returns (bool); + function enter(uint256 _amount) external; + function leave(uint256 _share) external; + function balanceOf(address account) external returns (uint256); + function decimals() external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/interfaces/IQuickswapV2Router02.sol b/contracts/interfaces/IQuickswapV2Router02.sol new file mode 100644 index 0000000..f125d06 --- /dev/null +++ b/contracts/interfaces/IQuickswapV2Router02.sol @@ -0,0 +1,22 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +interface IQuickswapV2Router02 { + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function getAmountsOut(uint256 amountIn, address[] calldata path) + external + view + returns (uint256[] memory amounts); + + function getAmountsIn(uint256 amountOut, address[] memory path) + external + view + returns (uint256[] memory amounts); +} diff --git a/contracts/interfaces/IUniswapV2Callee.sol b/contracts/interfaces/IUniswapV2Callee.sol new file mode 100644 index 0000000..d58f2e1 --- /dev/null +++ b/contracts/interfaces/IUniswapV2Callee.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IUniswapV2Callee { + function uniswapV2Call( + address sender, + uint256 amount0, + uint256 amount1, + bytes calldata data + ) external; +} + +interface IUniswapV2Factory { + function getPair(address token0, address token1) + external + view + returns (address); +} \ No newline at end of file diff --git a/contracts/interfaces/IVault.sol b/contracts/interfaces/IVault.sol new file mode 100644 index 0000000..24edf93 --- /dev/null +++ b/contracts/interfaces/IVault.sol @@ -0,0 +1,16 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +interface IVault { + function createVault() external returns (uint256); + function depositCollateral(uint256 vaultID, uint256 amount) external; + function withdrawCollateral(uint256 vaultID, uint256 amount) external; + function borrowToken(uint256 vaultID, uint256 amount) external; + function payBackToken(uint256 vaultID, uint256 amount) external; + function checkCollateralPercentage(uint256 vaultID) external view returns (uint256); + function _minimumCollateralPercentage() external view returns(uint256); + function getDebtCeiling() external view returns (uint256); + function vaultCollateral(uint256 vaultID) external view returns (uint256); + function vaultDebt(uint256 vaultID) external view returns (uint256); + function getEthPriceSource() external view returns (uint256); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9ea426c..24bc1da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,18 +7,19 @@ "dependencies": { "@openzeppelin/contracts": "^2.5.0", "@poanet/solidity-flattener": "^3.0.6", - "chai": "^4.3.4", - "chai-as-promised": "^7.1.1", "dotenv": "^8.2.0", "global": "^4.4.0", "hardhat": "^2.1.2", "hardhat-typechain": "^0.3.5", + "openzeppelin-4": "npm:@openzeppelin/contracts@^4.4.0", "truffle": "^5.2.6" }, "devDependencies": { "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-etherscan": "^2.1.1", "@nomiclabs/hardhat-waffle": "^2.0.1", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", "ethereum-waffle": "^3.3.0", "ethers": "^5.0.32", "mocha": "^9.1.2", @@ -5000,6 +5001,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, "engines": { "node": "*" } @@ -5645,6 +5647,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz", "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==", + "hasInstallScript": true, "dependencies": { "node-gyp-build": "^4.2.0" } @@ -5775,6 +5778,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", @@ -5791,6 +5795,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, "dependencies": { "check-error": "^1.0.2" }, @@ -5840,6 +5845,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, "engines": { "node": "*" } @@ -6562,6 +6568,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, "dependencies": { "type-detect": "^4.0.0" }, @@ -6825,6 +6832,7 @@ "version": "0.8.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "hasInstallScript": true, "optional": true, "dependencies": { "nan": "^2.14.0" @@ -9966,6 +9974,7 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz", "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==", "dev": true, + "hasInstallScript": true, "dependencies": { "node-gyp-build": "^4.2.0" } @@ -14027,6 +14036,7 @@ "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", "dev": true, + "hasInstallScript": true, "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0" @@ -15757,6 +15767,7 @@ "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz", "integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==", "dev": true, + "hasInstallScript": true, "dependencies": { "elliptic": "^6.5.2", "node-addon-api": "^2.0.0", @@ -17049,6 +17060,7 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.4.tgz", "integrity": "sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q==", "dev": true, + "hasInstallScript": true, "dependencies": { "node-gyp-build": "^4.2.0" } @@ -18155,6 +18167,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true, "engines": { "node": "*" } @@ -19989,6 +20002,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "hasInstallScript": true, "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0" @@ -20584,6 +20598,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.0.2.tgz", "integrity": "sha512-Ib6ygFYBleS8x2gh3C1AkVsdrUShqXpe6jSTnZ6sRycEXKhqVf+xOSkhgSnjidpPzyv0d95LJVFrYQ4NuXAqHA==", + "hasInstallScript": true, "optional": true, "dependencies": { "abstract-leveldown": "~6.0.0", @@ -22639,6 +22654,12 @@ "opencollective-postinstall": "index.js" } }, + "node_modules/openzeppelin-4": { + "name": "@openzeppelin/contracts", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.4.0.tgz", + "integrity": "sha512-dlKiZmDvJnGRLHojrDoFZJmsQVeltVeoiRN7RK+cf2FmkhASDEblE0RiaYdxPNsUZa6mRG8393b9bfyp+V5IAw==" + }, "node_modules/optimism": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.15.0.tgz", @@ -23135,6 +23156,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, "engines": { "node": "*" } @@ -25043,6 +25065,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz", "integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==", + "hasInstallScript": true, "dependencies": { "elliptic": "^6.5.2", "node-addon-api": "^2.0.0", @@ -25443,6 +25466,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz", "integrity": "sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg==", + "hasInstallScript": true, "optional": true, "dependencies": { "nan": "^2.12.1", @@ -26677,6 +26701,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, "engines": { "node": ">=4" } @@ -26980,6 +27005,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.4.tgz", "integrity": "sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q==", + "hasInstallScript": true, "dependencies": { "node-gyp-build": "^4.2.0" } @@ -32887,7 +32913,8 @@ "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true }, "async": { "version": "2.6.3", @@ -33581,6 +33608,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, "requires": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", @@ -33594,6 +33622,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, "requires": { "check-error": "^1.0.2" } @@ -33636,7 +33665,8 @@ "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true }, "checkpoint-store": { "version": "1.1.0", @@ -34270,6 +34300,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, "requires": { "type-detect": "^4.0.0" } @@ -44754,7 +44785,8 @@ "get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true }, "get-intrinsic": { "version": "1.1.1", @@ -48473,6 +48505,11 @@ "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", "optional": true }, + "openzeppelin-4": { + "version": "npm:@openzeppelin/contracts@4.4.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.4.0.tgz", + "integrity": "sha512-dlKiZmDvJnGRLHojrDoFZJmsQVeltVeoiRN7RK+cf2FmkhASDEblE0RiaYdxPNsUZa6mRG8393b9bfyp+V5IAw==" + }, "optimism": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.15.0.tgz", @@ -48888,7 +48925,8 @@ "pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true }, "pbkdf2": { "version": "3.1.1", @@ -51915,7 +51953,8 @@ "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true }, "type-fest": { "version": "0.21.3", diff --git a/package.json b/package.json index 723f4d0..f45dfbd 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,17 @@ "global": "^4.4.0", "hardhat": "^2.1.2", "hardhat-typechain": "^0.3.5", + "openzeppelin-4": "npm:@openzeppelin/contracts@^4.4.0", "truffle": "^5.2.6" }, "devDependencies": { "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-etherscan": "^2.1.1", "@nomiclabs/hardhat-waffle": "^2.0.1", - "ethereum-waffle": "^3.3.0", - "ethers": "^5.0.32", "chai": "^4.3.4", "chai-as-promised": "^7.1.1", + "ethereum-waffle": "^3.3.0", + "ethers": "^5.0.32", "mocha": "^9.1.2", "ts-node": "^9.1.1", "typescript": "^4.2.3" diff --git a/test/camDaiLeverageTest.js b/test/camDaiLeverageTest.js new file mode 100644 index 0000000..c307f53 --- /dev/null +++ b/test/camDaiLeverageTest.js @@ -0,0 +1,107 @@ +const { expect } = require("chai"); + +describe("camDaiLeverage", function () { + + let DAIAddr = "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063"; + let MAIAddr = "0xa3Fa99A148fA48D14Ed51d610c367C61876997F1"; + let amDAIAddr = "0x27F8D03b3a2196956ED754baDc28D73be8830A6e"; + let camDAIAddr = "0xE6C23289Ba5A9F0Ef31b8EB36241D5c800889b7b"; + let VaultAddr = "0xD2FE44055b5C874feE029119f70336447c8e8827"; + let DAIWhaleAddr = "0x5a08B7D899b9B54f4f0Bb2C1DC62eD031aedC6D1"; + let MAIWhaleAddr = "0x25864a712C80d33Ba1ad7c23CffA18b46F2fc00c"; + + before(async function () { + //Deploy + this.timeout(60000000); + + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [DAIWhaleAddr], + }); + + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [MAIWhaleAddr], + }); + + [this.account, this.anotherAccount] = await ethers.getSigners(); + this.DAIWhaleSigner = await ethers.getSigner(DAIWhaleAddr); + this.MAIWhaleSigner = await ethers.getSigner(MAIWhaleAddr); + + this.LeverageFactory = await ethers.getContractFactory("LeverageFactory"); + this.leverageFactory = await this.LeverageFactory.deploy(); + await this.leverageFactory.deployed(); + + this.camDaiLeverage = await ethers.getContractFactory("camDaiLeverage"); + + this.gERC20 = await ethers.getContractFactory("simpleErc20"); + await this.gERC20.attach(DAIAddr).connect(this.DAIWhaleSigner).transfer(this.account.address, ethers.utils.parseUnits("1000")); + await this.gERC20.attach(DAIAddr).connect(this.DAIWhaleSigner).transfer(this.anotherAccount.address, ethers.utils.parseUnits("1000")); + + //Increasing debt ceiling + await this.gERC20.attach(MAIAddr).connect(this.MAIWhaleSigner).transfer(VaultAddr, ethers.utils.parseUnits("50000")); + }); + + async function getbalances(context, camDaiLeverageAddr) { + console.log("\tamDAI balance: ", ethers.utils.formatUnits(await context.gERC20.attach(amDAIAddr).balanceOf(camDaiLeverageAddr))); + console.log("\tcamDAI balance: ", ethers.utils.formatUnits(await context.gERC20.attach(camDAIAddr).balanceOf(camDaiLeverageAddr))); + console.log("\tMAI balance: ", ethers.utils.formatUnits(await context.gERC20.attach(MAIAddr).balanceOf(camDaiLeverageAddr))); + console.log("\tDAI balance: ", ethers.utils.formatUnits(await context.gERC20.attach(DAIAddr).balanceOf(camDaiLeverageAddr))); + console.log("\tDAI balance (owner): ", ethers.utils.formatUnits(await context.gERC20.attach(DAIAddr).balanceOf(context.account.address))); + console.log("\tMAI balance (owner): ", ethers.utils.formatUnits(await context.gERC20.attach(MAIAddr).balanceOf(context.account.address))); + console.log("\tCollateral balance: ", ethers.utils.formatUnits(await context.camDaiLeverage.attach(camDaiLeverageAddr).getVaultCollateral())); + console.log("\tDebt balance: ", ethers.utils.formatUnits(await context.camDaiLeverage.attach(camDaiLeverageAddr).getVaultDebt())); + console.log("\tCollateral percentage: ", ethers.utils.formatUnits(await context.camDaiLeverage.attach(camDaiLeverageAddr).getCollateralPercentage(), "wei")); + } + + it("Should create from factory successfully, with `account`...", async function () { + await this.leverageFactory.connect(this.account).createNew(); + this.contractAddress = await this.leverageFactory.connect(this.account).getContractAddresses(this.account.address); + }); + + it("Do rulo, with `account`...", async function () { + this.timeout(60000000); + let toDeposit = ethers.utils.parseUnits("1000"); + await this.gERC20.attach(DAIAddr).connect(this.account).approve(this.contractAddress[0], toDeposit); + + await this.camDaiLeverage.attach(this.contractAddress[0]).connect(this.account).doRulo(toDeposit); + await getbalances(this, this.contractAddress[0]); + }); + + it("Undo rulo, with `account`...", async function () { + this.timeout(60000000); + await this.camDaiLeverage.attach(this.contractAddress[0]).connect(this.account).undoRulo(); + await getbalances(this, this.contractAddress[0]); + }); + + it("Should create from factory again successfully, with `account`...", async function () { + await this.leverageFactory.connect(this.account).createNew(); + let newContractAddressArr = await this.leverageFactory.connect(this.account).getContractAddresses(this.account.address); + expect(newContractAddressArr.length).greaterThan(this.contractAddress.length); + }); + + it("Should create from factory successfully, with `anotherAccount`...", async function () { + await this.leverageFactory.connect(this.anotherAccount).createNew(); + this.contractAddress = await this.leverageFactory.connect(this.anotherAccount).getContractAddresses(this.anotherAccount.address); + }); + + it("Do rulo, with `anotherAccount`...", async function () { + this.timeout(60000000); + let toDeposit = ethers.utils.parseUnits("1000"); + await this.gERC20.attach(DAIAddr).connect(this.anotherAccount).approve(this.contractAddress[0], toDeposit); + + await this.camDaiLeverage.attach(this.contractAddress[0]).connect(this.anotherAccount).doRulo(toDeposit); + }); + + it("Undo rulo, with `anotherAccount`...", async function () { + this.timeout(60000000); + await this.camDaiLeverage.attach(this.contractAddress[0]).connect(this.anotherAccount).undoRulo(); + }); + + it("Should create from factory again successfully, with `anotherAccount`...", async function () { + await this.leverageFactory.connect(this.anotherAccount).createNew(); + let newContractAddressArr = await this.leverageFactory.connect(this.anotherAccount).getContractAddresses(this.anotherAccount.address); + expect(newContractAddressArr.length).greaterThan(this.contractAddress.length); + }); + +}); \ No newline at end of file