Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions contracts/harvester/ITipJar.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.7.3;

interface ITipJar {
function tip() external payable;
function updateMinerSplit(address minerAddress, address splitTo, uint32 splitPct) external;
function setFeeCollector(address newCollector) external;
function setFee(uint32 newFee) external;
function changeAdmin(address newAdmin) external;
function upgradeTo(address newImplementation) external;
function upgradeToAndCall(address newImplementation, bytes calldata data) external payable;
}
18 changes: 12 additions & 6 deletions contracts/harvester/IUniswapRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ pragma solidity 0.7.3;

interface IUniswapRouter {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);

function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external;
}
117 changes: 117 additions & 0 deletions contracts/harvester/TipHarvester.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
pragma solidity 0.7.3;

import "@openzeppelin/contracts/math/SafeMath.sol";
import "../vault/IVault.sol";
import "./IUniswapRouter.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "./ITipJar.sol";

contract TipHarvester is Ownable {
using SafeMath for uint256;
using SafeERC20 for IERC20;
using Address for address payable;

IUniswapRouter public router;
ITipJar public immutable tipJar;
mapping(IVault => uint256) public ratePerToken;


constructor(IUniswapRouter router_, ITipJar tipJar_) {
router = router_;
tipJar = tipJar_;
}

fallback() external payable {}
receive() external payable {}

/***
* @notice retrieve tokens sent to contract by mistake
* @param token_ Token to retrieve
*/
function collect(address token_) public onlyOwner {
if (token_ == address(0)) {
payable(msg.sender).sendValue(address(this).balance);
} else {
uint256 balance = IERC20(token_).balanceOf(address(this));
IERC20(token_).safeTransfer(msg.sender, balance);
}
}

/**
* @notice approve to spend given amount of given token
* @param token Approval token
* @param spender Address to spend token
* @param amount Allowance of token amount to spend
*/
function _approve(IERC20 token, address spender, uint256 amount) internal {
if (token.allowance(address(this), spender) < amount) {
token.safeApprove(spender, amount);
}
}

/**
* @notice swap tokens for tokens
* @param amountIn Amount to swap
* @param amountOutMin Minimum amount out
* @param path Path for swap
* @param deadline Block timestamp deadline for trade
*/
function _swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) internal {
router.swapExactTokensForTokensSupportingFeeOnTransferTokens(amountIn, amountOutMin, path, to, deadline);
}

/**
* @notice Tip specific amount of ETH
* @param tipAmount Amount to tip
*/
function _tipAmountETH(uint256 tipAmount) internal {
tipJar.tip{value: tipAmount}();
}

/**
* @notice set router to IUniswapRouter compatible router
* @dev ensure router is IUniswapRouter compatible
* @param router_ IUniswapRouter compatible router
*/
function setRouter(IUniswapRouter router_) public onlyOwner {
router = router_;
}

/**
* @notice harvest vault while tipping miner
* _swapExactTokensForTokens returns no amounts and therefore received amount has to be calculated
* @param vault Vault from which to harvest
* @param amount Amount to harvest
* @param outMin Minimum amount of tokens out
* @param path Token swap path
* @param deadline Block timestamp deadline for trade
*/
function harvestVault(IVault vault, uint256 amount, uint256 outMin, address[] calldata path, uint256 deadline) public payable onlyOwner {
require(msg.value > 0, "Tip Harvester: tip must be > 0");
_tipAmountETH(msg.value);

uint256 afterFee = vault.harvest(amount);
uint256 durationSinceLastHarvest = block.timestamp.sub(vault.lastDistribution());
IERC20Detailed from = vault.underlying();
ratePerToken[vault] = afterFee.mul(10**(36-from.decimals())).div(vault.totalSupply()).div(durationSinceLastHarvest);
IERC20 to = vault.target();

_approve(from, address(router), afterFee);
uint256 toBalanceBefore = IERC20(path[path.length - 1]).balanceOf(address(this));
_swapExactTokensForTokens(afterFee, outMin, path, address(this), deadline);
uint256 toBalanceAfter = IERC20(path[path.length - 1]).balanceOf(address(this));

uint256 received = toBalanceAfter.sub(toBalanceBefore);
_approve(to, address(vault), received);
vault.distribute(received);
}
}
39 changes: 33 additions & 6 deletions hardhat.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { task } = require('hardhat/config')
const { submitArcherTransaction, getTip } = require('./src/utils')

require('@nomiclabs/hardhat-etherscan')
require('@nomiclabs/hardhat-waffle')
Expand Down Expand Up @@ -63,7 +64,7 @@ task('harvest', 'Harvest a vault')
const underlyingAddress = await vault.underlying()
const targetAddress = await vault.target()
const decimals = await vault.decimals()
const harvester = await ethers.getContractAt('UniswapHarvester', await vault.harvester())
const tipHarvester = await ethers.getContractAt('TipHarvester', await vault.harvester())
if (!args.amount) {
args.amount = await vault.callStatic.underlyingYield()
} else {
Expand All @@ -72,17 +73,34 @@ task('harvest', 'Harvest a vault')
if(args.amount.gt(0)) {
console.log("Harvesting", ethers.utils.formatUnits(args.amount, decimals))
const deadline = Math.ceil(Date.now()/1000) + 3600 // 1 hour from now
let path = [underlyingAddress, targetAddress];
let path = [underlyingAddress, targetAddress]
// TODO: Find best path dynamically
const weth = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
if(targetAddress.toLowerCase() !== weth.toLowerCase()) {
path = [underlyingAddress, weth, targetAddress]
}
const estimate = await harvester.estimateGas.harvestVault(args.vault, args.amount, 0, path, deadline)
const tx = await harvester.harvestVault(args.vault, args.amount, 0, path, deadline, {
gasLimit: Math.max(estimate, 1000000)
const provider = ethers.getDefaultProvider()
const estimate = await provider.estimateGas({
to: tipHarvester.address,
data: (await tipHarvester.populateTransaction.harvestVault(args.vault, args.amount, 0, path, deadline))['data']
})
console.log(tx.hash)

const tip = await getTip('standard')
// creating wallet as etherssigner.signTransaction not supported
const privateKey = (await provider.getNetwork()).chainId == 1 ? process.env.MAINNET_PRIVKEY : process.env.RINKEBY_PRIVKEY
const deployer = new ethers.Wallet(privateKey, provider)
const nonce = await deployer.getTransactionCount()
const unsignedTx = await tipHarvester.populateTransaction.harvestVault(args.vault, args.amount, 0, path, deadline, {
gasLimit: Math.max(estimate, 1000000),
nonce: nonce,
value: tip
})

// sign transaction and send to Archer DAO relay server
const signedTx = await deployer.signTransaction(unsignedTx)
const response = await submitArcherTransaction(signedTx, deadline)
const data = response.data;
console.log(`Response from Archer DAO relay, status: ${response.status}, data: ${JSON.stringify(data)}`)
} else {
console.log("Nothing to Harvest. Skipping.")
}
Expand Down Expand Up @@ -132,5 +150,14 @@ module.exports = {
version: '0.6.11'
}
]
},
namedAccounts: {
tipJar: {
1: '0x5312B0d160E16feeeec13437a0053009e7564287',
4: '0x914528335B5d031c93Ac86e6F6A6C67052Eb44f0'
},
deployer: {
defualt: 0
}
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@openzeppelin/contracts": "^3.3.0",
"@uniswap/merkle-distributor": "git://github.com/Uniswap/merkle-distributor.git#c3255bfa2b684594ecd562cacd7664b0f18330bf",
"async-prompt": "^1.0.1",
"axios": "^0.21.1",
"dotenv": "^8.2.0",
"openzeppelin-solidity-2.3.0": "npm:openzeppelin-solidity@2.3.0"
}
Expand Down
67 changes: 67 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require('dotenv').config();
const axios = require('axios');
const BASE_URL = 'https://api.archerdao.io';
const transactionPath = 'v1/transaction';
const gasPath = 'v1/gas';
let initBody = {
"jsonrpc": "2.0",
"method": "archer_submitTx"
}

async function submitArcherTransaction(tx, deadline) {
const url = `${BASE_URL}/${transactionPath}`;
const id = (new Date()).getTime();
const body = Object.assign({ tx, deadline, id }, initBody);
const response = await axios({
method: 'post',
url: url,
headers: {
'Authorization': process.env.ARCHER_DAO_API_KEY,
'Content-Type': 'application/json'
},
data: body
});
if (!response) {
console.log('Error sending transaction to Archer relay');
process.exit(1);
}

return response;
}

async function getArcherMinerTips() {
const url = `${BASE_URL}/${gasPath}`;
const response = await axios({
method: 'get',
url: url,
headers: {
'Content-Type': 'application/json',
'Referrer-Policy': 'no-referrer'
}
});

return response.data.data;
}

async function getTip(speed= 'standard') {
const tips = await getArcherMinerTips();
if (!tips) {
console.log(`Couldn't get Archer gas`);
process.exit(1);
}
switch (speed) {
case 'immediate': return tips['immediate'];
case 'rapid': return tips['rapid'];
case 'fast': return tips['fast']
case 'standard': return tips['standard'];
case 'slow': return tips['slow'];
case 'slower': return tips['slower'];
case 'slowest': return tips['slowest'];
default: return tips['standard'];
}
}

module.exports = {
submitArcherTransaction,
getTip
}