diff --git a/keepers-minimal/.eslintignore b/keepers-minimal/.eslintignore new file mode 100644 index 00000000..85f5562a --- /dev/null +++ b/keepers-minimal/.eslintignore @@ -0,0 +1,4 @@ +node_modules +artifacts +cache +coverage diff --git a/keepers-minimal/.eslintrc.js b/keepers-minimal/.eslintrc.js new file mode 100644 index 00000000..98ce1937 --- /dev/null +++ b/keepers-minimal/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + env: { + browser: false, + es2021: true, + mocha: true, + node: true, + }, + plugins: ["@typescript-eslint"], + extends: [ + "standard", + "plugin:prettier/recommended", + "plugin:node/recommended", + ], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 12, + }, + rules: { + "node/no-unsupported-features/es-syntax": [ + "error", + { ignores: ["modules"] }, + ], + }, +}; diff --git a/keepers-minimal/.gitignore b/keepers-minimal/.gitignore new file mode 100644 index 00000000..05b9053d --- /dev/null +++ b/keepers-minimal/.gitignore @@ -0,0 +1,12 @@ +node_modules +.env +coverage +coverage.json +typechain + +#Hardhat files +cache +artifacts + +yarn.lock +package-lock.json \ No newline at end of file diff --git a/keepers-minimal/.npmignore b/keepers-minimal/.npmignore new file mode 100644 index 00000000..dc037817 --- /dev/null +++ b/keepers-minimal/.npmignore @@ -0,0 +1,3 @@ +hardhat.config.ts +scripts +test diff --git a/keepers-minimal/.prettierignore b/keepers-minimal/.prettierignore new file mode 100644 index 00000000..f268596e --- /dev/null +++ b/keepers-minimal/.prettierignore @@ -0,0 +1,5 @@ +node_modules +artifacts +cache +coverage* +gasReporterOutput.json diff --git a/keepers-minimal/.prettierrc b/keepers-minimal/.prettierrc new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/keepers-minimal/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/keepers-minimal/.solhint.json b/keepers-minimal/.solhint.json new file mode 100644 index 00000000..f3e31e8c --- /dev/null +++ b/keepers-minimal/.solhint.json @@ -0,0 +1,7 @@ +{ + "extends": "solhint:recommended", + "rules": { + "compiler-version": ["error", "^0.8.0"], + "func-visibility": ["warn", { "ignoreConstructors": true }] + } +} diff --git a/keepers-minimal/.solhintignore b/keepers-minimal/.solhintignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/keepers-minimal/.solhintignore @@ -0,0 +1 @@ +node_modules diff --git a/keepers-minimal/README.md b/keepers-minimal/README.md new file mode 100644 index 00000000..edabaf9b --- /dev/null +++ b/keepers-minimal/README.md @@ -0,0 +1,266 @@ +# Keepers Minimal + +> **Warning** +> +> None of the contracts are audited! This repo is not production ready! + +This project demonstrates how Chainlink Keepers can be used in various types of DeFi projects. For the sake of simplicity, it automates some of the contracts from the [defi-minimal repository](https://github.com/smartcontractkit/defi-minimal). + +Read more [here](https://blog.chain.link/keepers-minimal). + +## Getting started + +### Prerequisites + +Be sure to have installed the following + +- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [Node.js](https://nodejs.org/en/download/) +- [Yarn](https://yarnpkg.com/getting-started/install) + +### Installation + +1. Clone the repo + +``` +git clone https://github.com/smartcontractkit/smart-contract-examples.git +``` + +2. Enter the directory + +``` +cd smart-contract-examples/keepers-minimal +``` + +### Build and Deploy + +1. Install packages + +```shell +yarn +``` + +2. Compile contracts + +```shell +yarn compile +``` + +3. Run tests + +```shell +yarn test +``` + +or + +```shell +yarn test --parallel +``` + +or + +```shell +REPORT_GAS=true yarn test +``` + +#### Performance optimizations + +For faster runs of your tests and scripts, consider skipping ts-node's type checking by setting the environment variable `TS_NODE_TRANSPILE_ONLY` to `1` in hardhat's environment. For more details see [the documentation](https://hardhat.org/guides/typescript.html#performance-optimizations). + +## Usage + +The project comes with three different smart contracts. + +### Passive Income Claimer + +This smart contract automates `Staking.sol` from [defi-minimal](https://github.com/smartcontractkit/defi-minimal), which itself is based off Synthetix protocol. + +It allows you to stake tokens and forget about them. It will automaticaly claim any passive income your stake earns over time and send it back to your wallet address. + +```mermaid +sequenceDiagram + actor User as User's EOA + participant Contract as PassiveIncomeClaimer.sol + participant Keepers as Chainlink Keepers + participant Staking as Staking.sol + + User->>Contract: Deploy and Set Reward Target + + User->>Contract: stake(amount) + Contract-->>User: Staked + + Note right of User: User can optionally adjust reward target any time + User->>Contract: adjustRewardTarget(rewardTarget) + Contract-->>User: TargetRewardAdjusted + + loop On each block, if earned > rewardTarget + Keepers->>Contract: checkUpkeep() + activate Contract + Contract->>Staking: earned(address(this)) + deactivate Contract + alt false + Contract-->>Keepers: Don't need an Upkeep + else true + Contract-->>Keepers: Upkeep Needed! + Keepers->>Contract: performUpkeep() + activate Contract + Contract->>Staking: claimReward() + Contract->>User: transfer(beneficary, reward) + Contract-->>User: PassiveIncomeClaimed + deactivate Contract + end + end + + User->>Contract: withdraw(amount) + Contract-->>User: Withdrawn +``` + +### Liquidation Saver + +This smart contract automates `Lending.sol` from [defi-minimal](https://github.com/smartcontractkit/defi-minimal), which itself is based off Aave protocol. + +It will save you from potentially liqiudations either by repaying your loan or depositing more collateral assets. + +```mermaid +sequenceDiagram + actor User as User's EOA + participant Contract as LiquidationSaver.sol + participant Keepers as Chainlink Keepers + participant Lending as Lending.sol + + User->>Contract: Deploy & Set minHealthFactor + + User->>Contract: depositTokenToSaver(tokenAddress, amount) + Contract-->>User: TokenDeposited + + Note right of User: User can Deposit + User->>Contract: deposit(tokenAddress, amount) + Contract->>Lending: deposit(tokenAddress, amount) + Lending-->>User: Deposit + + Note right of User: User can Withdraw + User->>Contract: withdraw(tokenAddress, amount) + Contract->>Lending: withdraw(tokenAddress, amount) + Lending-->>User: Withdraw + + Note right of User: User can Borrow + User->>Contract: borrow(tokenAddress, amount) + Contract->>Lending: borrow(tokenAddress, amount) + Lending-->>User: Borrow + + Note right of User: User can Repay + User->>Contract: repay(tokenAddress, amount) + Contract->>Lending: repay(tokenAddress, amount) + Lending-->>User: Repay + + loop On each block, if healthFactor < minHealthFactor + + Keepers->>Contract: checkUpkeep() + activate Contract + Contract->>Lending: healthFactor(address(this)) + deactivate Contract + + alt false + Contract-->>Keepers: Don't need an Upkeep + else true + Contract-->>Keepers: Upkeep Needed! + Keepers->>Contract: performUpkeep() + activate Contract + Contract->>Lending: getAccountInformation(address(this)) + Lending-->>Contract: (borrowedValueInETH, collateralValueInETH) + + Note right of Contract: To avoid liquidation one needs to either repay its loan or deposit more collateral + + alt Check do it has funds to repay loan + Contract->>Contract: _tryToRepay(borrowedValueInETH, borrowedTokenBalanceInSaver) + activate Contract + alt _borrowedTokenBalanceInSaver >= amountToRepay + Note right of Contract: Repay full Debt + Contract->>Lending: repay(tokenAddress, amountToRepay) + Lending-->Contract: Repay + else + Note right of Contract: Try to Repay Part of Debt + Contract->>Lending: repay(tokenAddress, _borrowedTokenBalanceInSaver) + Lending-->Contract: Repay + end + deactivate Contract + else Check do it has funds to deposit more + Contract->>Contract: _tryToDepositMore(collateralValueInETH, collateralTokenBalanceInSaver) + activate Contract + alt _collateralTokenBalanceInSaver >= 2 * collateralAmount + Note right of Contract: Try to Double the Collateral Value + Contract->>Lending: deposit(tokenAddress, 2 * collateralAmount) + Lending-->>Contract: Deposit + else + Note right of Contract: Deposit what's left in Saver + Contract->>Lending: deposit(tokenAddress, _collateralTokenBalanceInSaver) + Lending-->>Contract: Deposit + end + deactivate Contract + end + deactivate Contract + end + end + + User->>Contract: withdrawTokenFromSaver(tokenAddress, amount) + Contract-->>User: TokenWithdrawn +``` + +### Trading Bot + +This contract serves as an example of a fully on-chain trading bot of any given asset in terms of USD, supported by Chainlink Data Feeds. As an User you need to set token address, buying & selling price, and address of Chainlink Price Feeds Aggregator for a given asset in terms of USD. Also, you will need to deposit initial amount of both trading token and some stable token. + +If the price of a token in terms of USD is lower than a buying price, this contract will buy more tokens by swapping the stable tokens it poses for a token on Uniswap V3. If the price of a token in terms of USD is greater than selling price, this contract will sell tokens by swapping amount it poses for a defined stable token on Uniswap V3. + +```shell + buying price selling price + -∞ ----------------- | ---------------------------------- | ----------------- ∞ + ////// upkeep /////// ////// upkeep /////// +``` + +```mermaid +sequenceDiagram + actor User as User's EOA + participant Contract as TradingBot.sol + participant Keepers as Chainlink Keepers + participant PriceFeeds as Chainlink Data Feeds + participant UniswapV3 as Uniswap SwapRouter.sol + + User->>Contract: Deposit & Set Trading Token Address, Stable Token Address, Buying Price, Selling Price, Aggregator Address + + Note right of User: User should deposit initial amount of Trading Token + User->>Contract: deposit(tokenAddress, amount) + Contract-->>User: TokenDeposited + + Note right of User: User should deposit initial amount of Stable Token + User->>Contract: deposit(tokenAddress, amount) + Contract-->>User: TokenDeposited + + loop On each block, if currentPrice > sellingPrice || currentPrice < buyingPrice + Keepers->>Contract: checkUpkeep() + activate Contract + Contract->>Contract: getPrice() + Contract->>PriceFeeds: latestRoundData() + PriceFeeds->>Contract: currentPrice + deactivate Contract + alt false + Contract-->>Keepers: Don't need an Upkeep + else true + Contract-->>Keepers: Upkeep Needed! + Keepers->>Contract: performUpkeep() + activate Contract + alt currentPrice < buyingPrice + Note right of Contract: Buy + Contract->>UniswapV3: swap(tokenIn = stableToken, tokenOut = tradingToken) + else currentPrice > sellingPrice + Note right of Contract: Sell + Contract->>UniswapV3: swap(tokenIn = tradingToken, tokenOut = stableToken) + end + deactivate Contract + end + end + + User->>Contract: withdraw(tokenAddress, amount) + Contract-->>User: TokenWithdrawn +``` diff --git a/keepers-minimal/contracts/LiquidationSaver.sol b/keepers-minimal/contracts/LiquidationSaver.sol new file mode 100644 index 00000000..fd51b38f --- /dev/null +++ b/keepers-minimal/contracts/LiquidationSaver.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./interfaces/ILending.sol"; + +contract LiquidationSaver is KeeperCompatibleInterface, Ownable { + address public s_beneficary; + address public s_lendingAddress; + address public s_borrowedTokenAddress; + address public s_collateralTokenAddress; + uint256 public immutable i_minHealthFactor; + + event EthDeposited(uint256 indexed amount); + event TokenDeposited(address indexed tokenAddress, uint256 indexed amount); + event EthWithdrawn(uint256 indexed amount); + event TokenWithdrawn(address indexed tokenAddress, uint256 indexed amount); + + constructor( + address _beneficary, + address _lendingAddress, + uint256 _minHealthFactor + ) { + s_beneficary = _beneficary; + s_lendingAddress = _lendingAddress; + i_minHealthFactor = _minHealthFactor; + } + + /************************************/ + /* Saver Assets Managment Functions */ + /************************************/ + + receive() external payable { + emit EthDeposited(msg.value); + } + + function depositTokenToSaver(address _tokenAddress, uint256 _amount) + external + onlyOwner + { + IERC20(_tokenAddress).transferFrom(msg.sender, address(this), _amount); + + emit TokenDeposited(_tokenAddress, _amount); + } + + function withdrawEthFromSaver(uint256 _amount) external onlyOwner { + require(_amount >= address(this).balance); + + (bool success, ) = s_beneficary.call{value: _amount}(""); + require(success); + + emit EthWithdrawn(_amount); + } + + function withdrawTokenFromSaver(address _tokenAddress, uint256 _amount) + external + onlyOwner + { + require(_amount >= IERC20(_tokenAddress).balanceOf(address(this))); + + IERC20(_tokenAddress).transfer(s_beneficary, _amount); + + emit TokenWithdrawn(_tokenAddress, _amount); + } + + /******************************************/ + /* Chainlink Keepers Automation Functions */ + /******************************************/ + + function checkUpkeep(bytes calldata checkData) + external + view + override + returns (bool upkeepNeeded, bytes memory performData) + { + upkeepNeeded = + ILending(s_lendingAddress).healthFactor(address(this)) < + i_minHealthFactor; + } + + function performUpkeep(bytes calldata performData) external override { + require( + ILending(s_lendingAddress).healthFactor(address(this)) < + i_minHealthFactor, + "Account can't be liquidated!" + ); + + /** + * To avoid liquidation you can raise your health factor by + * 1. repaying part of your loan or + * 2. depositing more collateral assets. + */ + + (uint256 borrowedValueInETH, uint256 collateralValueInETH) = ILending( + s_lendingAddress + ).getAccountInformation(address(this)); + + + uint256 borrowedTokenBalanceInSaver = IERC20(s_borrowedTokenAddress) + .balanceOf(address(this)); + uint256 collateralTokenBalanceInSaver = IERC20(s_collateralTokenAddress) + .balanceOf(address(this)); + + // Check do you have funds to repay + if (borrowedTokenBalanceInSaver > 0) { + _tryToRepay(borrowedValueInETH, borrowedTokenBalanceInSaver); + } else { + // Check do you have funds to deposit more collateral + if (collateralTokenBalanceInSaver > 0) { + _tryToDepositMore( + collateralValueInETH, + collateralTokenBalanceInSaver + ); + } + } + + + } + + function _tryToRepay( + uint256 _borrowedValueInETH, + uint256 _borrowedTokenBalanceInSaver + ) private { + uint256 amountToRepay = ILending(s_lendingAddress).getTokenValueFromEth( + s_borrowedTokenAddress, + _borrowedValueInETH + ); + + if (_borrowedTokenBalanceInSaver >= amountToRepay) { + // repay full debt + ILending(s_lendingAddress).repay( + s_borrowedTokenAddress, + amountToRepay + ); + } else { + // try to repay part of debt + ILending(s_lendingAddress).repay( + s_borrowedTokenAddress, + _borrowedTokenBalanceInSaver + ); + } + } + + function _tryToDepositMore( + uint256 _collateralValueInETH, + uint256 _collateralTokenBalanceInSaver + ) private { + uint256 collateralAmount = ILending(s_lendingAddress) + .getTokenValueFromEth( + s_collateralTokenAddress, + _collateralValueInETH + ); + + // try to double collateral value + if (_collateralTokenBalanceInSaver >= 2 * collateralAmount) { + ILending(s_lendingAddress).deposit( + s_collateralTokenAddress, + 2 * collateralAmount + ); + } else { + // deposit what's left in the saver + ILending(s_lendingAddress).deposit( + s_collateralTokenAddress, + _collateralTokenBalanceInSaver + ); + } + } + + /*******************************************************/ + /* Functions to Interact with DeFi Minimal Lending.sol */ + /*******************************************************/ + + function deposit(address _tokenAddress, uint256 _amount) + external + onlyOwner + { + require(_amount <= IERC20(_tokenAddress).balanceOf(address(this))); + ILending(s_lendingAddress).deposit(_tokenAddress, _amount); + s_collateralTokenAddress = _tokenAddress; + } + + // @notice Withdraws tokens from Lending.sol to LiquidtaionSaver.sol + function withdraw(address _tokenAddress, uint256 _amount) + external + onlyOwner + { + ILending(s_lendingAddress).withdraw(_tokenAddress, _amount); + } + + function borrow(address _tokenAddress, uint256 _amount) external onlyOwner { + ILending(s_lendingAddress).borrow(_tokenAddress, _amount); + s_borrowedTokenAddress = _tokenAddress; + } + + function repay(address _tokenAddress, uint256 _amount) external onlyOwner { + ILending(s_lendingAddress).repay(_tokenAddress, _amount); + } +} diff --git a/keepers-minimal/contracts/PassiveIncomeClaimer.sol b/keepers-minimal/contracts/PassiveIncomeClaimer.sol new file mode 100644 index 00000000..59d924b8 --- /dev/null +++ b/keepers-minimal/contracts/PassiveIncomeClaimer.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./interfaces/IStaking.sol"; + +contract PassiveIncomeClaimer is KeeperCompatibleInterface, Ownable { + address public s_beneficary; + address public s_stakerAddress; + IERC20 public s_rewardsToken; + uint256 public s_rewardTarget; + + event Staked(uint256 amount); + event Withdrawn(uint256 amount); + event TargetRewardAdjusted(uint256 reward); + event PassiveIncomeClaimed(uint256 amount); + + constructor( + address _beneficary, + address _stakerAddress, + address _rewardsTokenAddress, + uint256 _rewardTarget + ) { + s_beneficary = _beneficary; + s_stakerAddress = _stakerAddress; + s_rewardsToken = IERC20(_rewardsTokenAddress); + s_rewardTarget = _rewardTarget; + } + + function stake(uint256 amount) external onlyOwner { + s_rewardsToken.transferFrom(msg.sender, address(this), amount); + + IStaking(s_stakerAddress).stake(amount); + + emit Staked(amount); + } + + function adjustRewardTarget(uint256 rewardTarget) external onlyOwner { + s_rewardTarget = rewardTarget; + + emit TargetRewardAdjusted(rewardTarget); + } + + function withdraw(uint256 amount) external onlyOwner { + IStaking(s_stakerAddress).withdraw(amount); + + s_rewardsToken.transfer(msg.sender, amount); + + emit Withdrawn(amount); + } + + function checkUpkeep( + bytes calldata /*checkData*/ + ) + external + view + override + returns ( + bool upkeepNeeded, + bytes memory /*performData*/ + ) + { + upkeepNeeded = + IStaking(s_stakerAddress).earned(address(this)) >= s_rewardTarget; + } + + function performUpkeep( + bytes calldata /*performData*/ + ) external override { + uint256 reward = IStaking(s_stakerAddress).earned(address(this)); + require( + reward >= s_rewardTarget + ); + + IStaking(s_stakerAddress).claimReward(); + + s_rewardsToken.transfer( + s_beneficary, + s_rewardsToken.balanceOf(address(this)) + ); + + emit PassiveIncomeClaimed(reward); + } +} diff --git a/keepers-minimal/contracts/TradingBot.sol b/keepers-minimal/contracts/TradingBot.sol new file mode 100644 index 00000000..91e5ae6a --- /dev/null +++ b/keepers-minimal/contracts/TradingBot.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol"; +import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; + +/** + * @notice Parameters necessary for Uniswap V3 swap + * + * @param deadline - transaction will revert if it is pending for more than this period of time + * @param fee - the fee of the token pool to consider for the pair + * @param sqrtPriceLimitX96 - the price limit of the pool that cannot be exceeded by the swap + * @param isMultiSwap - flag to check whether to perform single or multi swap, cheaper than to compare path with abi.encodePacked("") + * @param path - sequence of (tokenAddress - fee - tokenAddress), encoded in reverse order, which are the variables needed to compute each pool contract address in sequence of swaps + * + * @dev path is encoded in reverse order + */ +struct SwapParameters { + uint256 deadline; + uint24 fee; + uint160 sqrtPriceLimitX96; + bool isMultiSwap; + bytes path; +} + +contract TradingBot is KeeperCompatibleInterface, Ownable { + /** + * @notice Represents the token which this bot contract trades + * + * @param tokenAddress - address of erc20 token contract + * @param tokenInTermsOfUsdAggregator - $TOKEN / USD Chainlink Price Feed Aggregator + * @param buyingPrice - if the price of the token in terms of USD is less than this value bot should buy + * @param sellingPrice - if the price of the token in terms of USD is greater than this value bot should sell + * + * @dev Buying is being done by swapping stable coin for $TOKEN + * @dev Selling is being done by swapping $TOKEN for stable coin + */ + struct Token { + address tokenAddress; + AggregatorV3Interface tokenInTermsOfUsdAggregator; + uint256 buyingPrice; + uint256 sellingPrice; + SwapParameters buyingParams; + SwapParameters sellingParams; + } + + address private s_stableToken; + Token private s_tradingToken; + ISwapRouter public constant UNISWAP_V3_ROUTER_CONTRACT = + ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); // It's the same on each network + + event TokenDeposited(address indexed tokenAddress, uint256 indexed amount); + event TokenWithdrawn(address indexed tokenAddress, uint256 indexed amount); + + constructor( + address _stableToken, + address _tradingTokenAddress, + address _aggregatorAddress, + uint256 _buyingPrice, + uint256 _sellingPrice, + SwapParameters memory _buyingParams, + SwapParameters memory _sellingParams + ) { + s_stableToken = _stableToken; + setTradingToken( + _tradingTokenAddress, + _aggregatorAddress, + _buyingPrice, + _sellingPrice, + _buyingParams, + _sellingParams + ); + } + + function setTradingToken( + address _tradingTokenAddress, + address _aggregatorAddress, + uint256 _buyingPrice, + uint256 _sellingPrice, + SwapParameters memory _buyingParams, + SwapParameters memory _sellingParams + ) public onlyOwner { + s_tradingToken = Token( + _tradingTokenAddress, + AggregatorV3Interface(_aggregatorAddress), + _buyingPrice, + _sellingPrice, + _buyingParams, + _sellingParams + ); + } + + function deposit(address _tokenAddress, uint256 _amount) + external + onlyOwner + { + IERC20(_tokenAddress).transferFrom(msg.sender, address(this), _amount); + emit TokenDeposited(_tokenAddress, _amount); + } + + function withdraw(address _tokenAddress, uint256 _amount) + external + onlyOwner + { + IERC20(_tokenAddress).transfer(msg.sender, _amount); + emit TokenWithdrawn(_tokenAddress, _amount); + } + + function getPrice() public view returns (int256) { + (, int256 price, , , ) = s_tradingToken + .tokenInTermsOfUsdAggregator + .latestRoundData(); + + return price; + } + + function checkUpkeep(bytes calldata checkData) + external + view + override + returns (bool upkeepNeeded, bytes memory performData) + { + uint256 currentPrice = uint256(getPrice()); + upkeepNeeded = + currentPrice > s_tradingToken.sellingPrice || + currentPrice < s_tradingToken.buyingPrice; + + /** + * buying price selling price + * -∞ ----------------- | ---------------------------------- | ----------------- ∞ + * ////// upkeep /////// ////// upkeep /////// + * + */ + } + + function performUpkeep(bytes calldata performData) external override { + uint256 currentPrice = uint256(getPrice()); + require( + currentPrice > s_tradingToken.sellingPrice || + currentPrice < s_tradingToken.buyingPrice, + "Trading is not profitable right now" + ); + + if (currentPrice < s_tradingToken.buyingPrice) { + // buy + uint256 amountIn = IERC20(s_stableToken).balanceOf(address(this)); + swap( + s_stableToken, + s_tradingToken.tokenAddress, + amountIn, + s_tradingToken.buyingPrice, + s_tradingToken.buyingParams + ); + } else { + // sell + uint256 tradingTokenBalance = IERC20(s_tradingToken.tokenAddress) + .balanceOf(address(this)); + + uint256 amountIn = (currentPrice * tradingTokenBalance) / + s_tradingToken.tokenInTermsOfUsdAggregator.decimals(); + + swap( + s_tradingToken.tokenAddress, + s_stableToken, + amountIn, + s_tradingToken.sellingPrice, + s_tradingToken.sellingParams + ); + } + } + + function swap( + address _tokenIn, + address _tokenOut, + uint256 _amountIn, + uint256 _minimumAmountOut, + SwapParameters memory _params + ) internal { + TransferHelper.safeApprove( + _tokenIn, + address(UNISWAP_V3_ROUTER_CONTRACT), + _amountIn + ); + + if (_params.isMultiSwap) { + ISwapRouter.ExactInputParams memory params = ISwapRouter + .ExactInputParams({ + path: _params.path, // @dev to swap DAI for WETH9 through a USDC pool: abi.encodePacked(WETH9, poolFee, USDC, poolFee, DAI) + recipient: address(this), + deadline: _params.deadline, + amountIn: _amountIn, + amountOutMinimum: _minimumAmountOut + }); + + UNISWAP_V3_ROUTER_CONTRACT.exactInput(params); + } else { + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter + .ExactInputSingleParams({ + tokenIn: _tokenIn, + tokenOut: _tokenOut, + fee: _params.fee, + recipient: address(this), + deadline: _params.deadline, + amountIn: _amountIn, + amountOutMinimum: _minimumAmountOut, + sqrtPriceLimitX96: _params.sqrtPriceLimitX96 + }); + + UNISWAP_V3_ROUTER_CONTRACT.exactInputSingle(params); + } + + TransferHelper.safeApprove( + _tokenIn, + address(UNISWAP_V3_ROUTER_CONTRACT), + 0 + ); + } +} diff --git a/keepers-minimal/contracts/interfaces/ILending.sol b/keepers-minimal/contracts/interfaces/ILending.sol new file mode 100644 index 00000000..ac5d15a4 --- /dev/null +++ b/keepers-minimal/contracts/interfaces/ILending.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +interface ILending { + function deposit(address token, uint256 amount) external; + + function withdraw(address token, uint256 amount) external; + + function borrow(address token, uint256 amount) external; + + function liquidate( + address account, + address repayToken, + address rewardToken + ) external; + + function repay(address token, uint256 amount) external; + + function getAccountInformation(address user) + external + view + returns (uint256 borrowedValueInETH, uint256 collateralValueInETH); + + function getAccountCollateralValue(address user) + external + view + returns (uint256); + + function getAccountBorrowedValue(address user) + external + view + returns (uint256); + + function getEthValue(address token, uint256 amount) + external + view + returns (uint256); + + function getTokenValueFromEth(address token, uint256 amount) + external + view + returns (uint256); + + function healthFactor(address account) external view returns (uint256); +} diff --git a/keepers-minimal/contracts/interfaces/IStaking.sol b/keepers-minimal/contracts/interfaces/IStaking.sol new file mode 100644 index 00000000..f6f687d9 --- /dev/null +++ b/keepers-minimal/contracts/interfaces/IStaking.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +interface IStaking { + /** + * @notice How much reward a token gets based on how long it's been in and during which "snapshots" + */ + function rewardPerToken() external view returns (uint256); + + /** + * @notice How much reward a user has earned + */ + function earned(address account) external view returns (uint256); + + /** + * @notice Deposit tokens into Staking contract + * @param amount | How much to stake + */ + function stake(uint256 amount) external; + + /** + * @notice Withdraw tokens from Staking contract + * @param amount | How much to withdraw + */ + function withdraw(uint256 amount) external; + + /** + * @notice User claims their tokens + */ + function claimReward() external; + + function getStaked(address account) external view returns (uint256); +} diff --git a/keepers-minimal/hardhat.config.ts b/keepers-minimal/hardhat.config.ts new file mode 100644 index 00000000..e7ab23df --- /dev/null +++ b/keepers-minimal/hardhat.config.ts @@ -0,0 +1,20 @@ +import * as dotenv from "dotenv"; + +import { HardhatUserConfig } from "hardhat/config"; +import "@nomiclabs/hardhat-etherscan"; +import "@nomiclabs/hardhat-waffle"; +import "@typechain/hardhat"; +import "hardhat-gas-reporter"; +import "solidity-coverage"; + +dotenv.config(); + +const config: HardhatUserConfig = { + solidity: "0.8.7", + gasReporter: { + enabled: process.env.REPORT_GAS !== undefined, + currency: "USD", + }, +}; + +export default config; diff --git a/keepers-minimal/package.json b/keepers-minimal/package.json new file mode 100644 index 00000000..019849c7 --- /dev/null +++ b/keepers-minimal/package.json @@ -0,0 +1,45 @@ +{ + "name": "keepers-minimal", + "license": "MIT", + "scripts": { + "compile": "hardhat compile", + "test": "hardhat test" + }, + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.0.6", + "@nomiclabs/hardhat-etherscan": "^3.1.0", + "@nomiclabs/hardhat-waffle": "^2.0.3", + "@typechain/ethers-v5": "^7.2.0", + "@typechain/hardhat": "^2.3.1", + "@types/chai": "^4.3.1", + "@types/mocha": "^9.1.1", + "@types/node": "^12.20.55", + "@typescript-eslint/eslint-plugin": "^4.33.0", + "@typescript-eslint/parser": "^4.33.0", + "@uniswap/v3-periphery": "^1.4.1", + "chai": "^4.3.6", + "dotenv": "^16.0.1", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.5.0", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^3.4.1", + "eslint-plugin-promise": "^5.2.0", + "ethereum-waffle": "^3.4.4", + "ethers": "^5.6.8", + "hardhat": "^2.9.9", + "hardhat-gas-reporter": "^1.0.8", + "prettier": "^2.6.2", + "prettier-plugin-solidity": "^1.0.0-beta.13", + "solhint": "^3.3.7", + "solidity-coverage": "^0.7.21", + "ts-node": "^10.8.1", + "typechain": "^5.2.0", + "typescript": "^4.7.3" + }, + "dependencies": { + "@chainlink/contracts": "^0.4.1", + "@openzeppelin/contracts": "^4.6.0" + } +} diff --git a/keepers-minimal/tsconfig.json b/keepers-minimal/tsconfig.json new file mode 100644 index 00000000..47f802e4 --- /dev/null +++ b/keepers-minimal/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "declaration": true + }, + "include": ["./scripts", "./test", "./typechain"], + "files": ["./hardhat.config.ts"] +}