Skip to content

Instantly share code, notes, and snippets.

@smatthewenglish
Created September 21, 2023 17:34
Show Gist options
  • Save smatthewenglish/ac3ac2669f901b4e70e6c4196a1ff0cb to your computer and use it in GitHub Desktop.
Save smatthewenglish/ac3ac2669f901b4e70e6c4196a1ff0cb to your computer and use it in GitHub Desktop.
// 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