Created
September 21, 2023 17:34
-
-
Save smatthewenglish/ac3ac2669f901b4e70e6c4196a1ff0cb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.20; | |
import { Ownable2Step } from "../lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol"; | |
import { ReentrancyGuard } from "../lib/openzeppelin-contracts/contracts/security/ReentrancyGuard.sol"; | |
///@dev values for MONTHS and YEARS not included because they're not uniform | |
enum TimeUnit{ MINUTES, HOURS, DAYS, WEEKS } | |
interface IFixedWindowOracle { | |
function update() external; | |
function consult(address token, uint256 amountIn) external view returns (uint256 amountOut); | |
} | |
interface IUniswapV2Pair { | |
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); | |
} | |
interface IUniswapV2Router02 { | |
function WETH() external pure returns (address); | |
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) external pure returns (uint256 amountOut); | |
function swapExactETHForTokens(uint256 amountOutMin, address[] memory path, address to, uint256 deadline) external payable returns (uint256[] memory amounts); | |
} | |
/** | |
* ,,))))))));, | |
* __)))))))))))))), | |
\|/ -\(((((''''((((((((. | |
-*-==//////(('' . `)))))), | |
/|\ ))| o ;-. '((((( | |
* ( `| / ) ;))))' | |
* | | | ,))(((( | |
* o_); ; )))(((` | |
* ; ''''````` | |
* @title Incentivized Manipulation-Resistant Dca | |
* @author Sean Matt English | |
* @notice has dependency on FixedWindowOracle | |
* the set-up process is as follows: | |
* a) determine, or deploy, a UniswapV2Pair on UniswapV2Factory | |
* b) deploy FixedWindowOracle with reference to that pair | |
* c) deploy IncentivizedResistantDca with reference to that pair, | |
* oracle and the router | |
* d) using the deployment key from step b), set the IncentivizedResistantDca | |
* as the owner of the FixedWindowOracle | |
* e) call initialize on the IncentivizedResistantDca with the token to DCA | |
* into, as well as the interval and unit | |
* NOTE: ^the contract is currently in this state, with interval set to 3, | |
* unit set to MINUTES, and a starting balance of 1.5 eth | |
* f) execute the DCA strategy | |
*/ | |
contract IncentivizedResistantDca is Ownable2Step, ReentrancyGuard { | |
bool public _initialized; | |
uint256 public _balance; | |
uint256 public _period; | |
uint256 public _interval; // Number of time units between market sells | |
TimeUnit public _unit; // The unit in which the purchase frequency applies | |
address public _addressWeth; | |
IUniswapV2Router02 public _router; | |
uint256 public _acceptableOracleDelta; | |
IFixedWindowOracle public _oracle; | |
address public _token; | |
IUniswapV2Pair public _pair; | |
uint256 public _amountIn; | |
uint256 public _lastExecutionTime; | |
uint256 public _fee; | |
uint256 public constant _incentiveFee = 3e15; // 0.3% | |
bool public _incentiveFeeOn; | |
uint256 public constant SCALING_FACTOR = 1e18; | |
string private constant INITIALIZED = "IncentivizedResistantDca: Already initialized"; | |
string private constant INSUFFICIENT_FUNDS = "IncentivizedResistantDca: Insufficient funds available"; | |
event IncentiveFee(bool incentiveFeeOn_); | |
event Withdrawn(address owner, uint256 amount); | |
event AcceptableOracleDelta(uint256 acceptableOracleDelta_); | |
/** | |
* | |
* @param pair_ the pair on which to perform the DCA strategy | |
* @param router_ swap execution and amountOut calculation | |
* @param oracle_ if deployed with another oracle it's important that the periods match | |
*/ | |
constructor( | |
IUniswapV2Pair pair_, | |
IUniswapV2Router02 router_, | |
IFixedWindowOracle oracle_ | |
) { | |
_pair = pair_; | |
_router = router_; | |
_addressWeth = router_.WETH(); | |
_oracle = oracle_; | |
_acceptableOracleDelta = 0.1e17; ///@dev default value small, since test pair has low | |
///liquidity adjustable via setAcceptableOracleDelta | |
_incentiveFeeOn = true; | |
_transferOwnership(msg.sender); | |
} | |
/** | |
* accept a one time eth payment | |
* @param token_ ERC20 token to DCA into | |
* @param interval_ number of units between market sells | |
* @param unit_ time unit enum | |
*/ | |
function initialize( | |
address token_, | |
uint256 interval_, | |
TimeUnit unit_ | |
) external payable onlyOwner { | |
require(!_initialized, INITIALIZED); | |
_initialized = true; | |
_unit = unit_; | |
_token = token_; | |
_interval = interval_; | |
_balance = msg.value; | |
_lastExecutionTime = block.timestamp; | |
_oracle.update(); | |
if(_incentiveFeeOn) { | |
_calculateAmountInWithFee(_balance, _interval); | |
} else { | |
_amountIn = _balance / _interval; | |
} | |
if (_unit == TimeUnit.WEEKS) { | |
_period = _interval * 1 weeks; | |
} else if (_unit == TimeUnit.DAYS) { | |
_period = _interval * 1 days; | |
} else if (_unit == TimeUnit.HOURS) { | |
_period = _interval * 1 hours; | |
} else { | |
_period = _interval * 1 minutes; | |
} | |
} | |
/** | |
* @dev determine the fee per interval and deduct the fee to get the actual | |
* trade amount | |
*/ | |
function _calculateAmountInWithFee(uint256 ethReceived, uint256 numIntervals) private { | |
uint256 _amountInWithoutFee = ethReceived / numIntervals; | |
_fee = (_amountInWithoutFee * _incentiveFee) / SCALING_FACTOR; | |
_amountIn = _amountInWithoutFee - _fee; | |
} | |
/** | |
*/ | |
function executeMarketSellSpecifyPath(address[] calldata path) external nonReentrant { | |
_swapExactETHForTokens(path); | |
} | |
/** | |
*/ | |
function executeMarketSell() external nonReentrant { | |
address[] memory path = new address[](2); | |
path[0] = _addressWeth; | |
path[1] = _token; | |
_swapExactETHForTokens(path); | |
} | |
/** | |
*/ | |
function _swapExactETHForTokens(address[] memory path) private { | |
require(address(this).balance >= _amountIn + _fee, INSUFFICIENT_FUNDS); | |
uint256 timePassed = block.timestamp - _lastExecutionTime; | |
require(timePassed >= _period, "IncentivizedResistantDca: Not enough time has passed since the last execution"); | |
///@dev assumes token0 is _token, token1 is WETH | |
(uint112 reserveOut, uint112 reserveIn, ) = _pair.getReserves(); | |
uint256 amountOutMin = _router.getAmountOut(_amountIn, reserveIn, reserveOut); | |
address owner = owner(); | |
uint256 deadline = block.timestamp + 1 hours; | |
uint256[] memory amounts = _router.swapExactETHForTokens{value: _amountIn}(amountOutMin, path, owner, deadline); | |
_oracle.update(); | |
uint256 twap = _oracle.consult(_token, _amountIn); | |
uint256 acceptableMinimum = (twap * _acceptableOracleDelta) / SCALING_FACTOR; | |
require(amounts[1] >= acceptableMinimum, "IncentivizedResistantDca: Price is too far from TWAP"); | |
_balance -= _amountIn; | |
_lastExecutionTime = block.timestamp; | |
_payIncentiveFee(owner, msg.sender); | |
} | |
/** | |
* @dev the fee is only paid out if the caller is not the owner, | |
* since the owner is already incentivized to execute the DCA strategy | |
*/ | |
function _payIncentiveFee(address owner, address caller) private { | |
if(owner == caller || !_incentiveFeeOn) { | |
return; | |
} | |
require(address(this).balance >= _fee, INSUFFICIENT_FUNDS); | |
_safeTransfer(caller, _fee); | |
_balance -= _fee; | |
} | |
function _safeTransfer(address to, uint256 amount) private { | |
(bool success,) = payable(to).call{value: amount}(""); | |
require(success, "IncentivizedResistantDca: Transfer failed"); | |
} | |
/** | |
* @dev this method can end the DCA strategy early, or recover | |
* funds that remain in the contract if fees were on but the owner | |
* invoked a market sell | |
*/ | |
function withdraw(uint256 value) external onlyOwner { | |
require(address(this).balance >= value, INSUFFICIENT_FUNDS); | |
address owner = owner(); | |
_safeTransfer(owner, value); | |
emit Withdrawn(owner, value); | |
} | |
/** | |
* @dev toggle the fee on and off, the max amount of fee is hardcoded | |
*/ | |
function setIncentiveFee(bool incentiveFeeOn_) external onlyOwner { | |
require(!_initialized, INITIALIZED); | |
_incentiveFeeOn = incentiveFeeOn_; | |
emit IncentiveFee(_incentiveFeeOn); | |
} | |
/** | |
* @param acceptableOracleDelta_ accounts for liquidity provider fee, price impact & slippage tolerance | |
*/ | |
function setAcceptableOracleDelta(uint256 acceptableOracleDelta_) external onlyOwner { | |
_acceptableOracleDelta = acceptableOracleDelta_; | |
emit AcceptableOracleDelta(_acceptableOracleDelta); | |
} | |
/** | |
* @dev convenience method for determining the next execution time | |
*/ | |
function secondsUntilNextExecution() external view returns (uint256 seconds_) { | |
uint256 timePassed = block.timestamp - _lastExecutionTime; | |
if (timePassed >= _period) { | |
seconds_ = 0; | |
} | |
seconds_ = _period - timePassed; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment