Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Oracle final #42

Closed
wants to merge 8 commits into from
Closed
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
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ number_underscore = 'thousands'
multiline_func_header = 'params_first'

[profile.default]
solc = '0.8.19'
solc = '0.8.20'
evm_version = 'paris'
src = 'solidity'
test = 'solidity/test'
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@
"package.json": "sort-package-json"
},
"dependencies": {
"@openzeppelin/contracts": "^5.0.0",
"ds-test": "github:dapphub/ds-test#e282159",
"forge-std": "github:foundry-rs/forge-std#v1.5.6",
"isolmate": "github:defi-wonderland/isolmate#59e1804",
"prb/test": "github:paulrberg/prb-test#a245c71"
"prb/test": "github:paulrberg/prb-test#a245c71",
"uniswap": "^0.0.1"
},
"devDependencies": {
"@commitlint/cli": "17.0.3",
Expand Down
4 changes: 3 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ isolmate/=node_modules/isolmate/src

contracts/=solidity/contracts
interfaces/=solidity/interfaces
test/=solidity/test
test/=solidity/test
@openzeppelin/=node_modules/@openzeppelin
@uniswap/=node_modules/uniswap/uniswap/contracts
59 changes: 0 additions & 59 deletions solidity/contracts/Greeter.sol

This file was deleted.

73 changes: 73 additions & 0 deletions solidity/contracts/Oracle/Oracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
pragma solidity ^0.8.19;

import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {IWrapper} from '../../interfaces/Oracle/IWrapper.sol';
import {IOracle} from '../../interfaces/Oracle/IOracle.sol';

contract Oracle is Ownable, IOracle {
mapping(address _tokenA => mapping(address _tokenB => IWrapper _wrapper)) public pairWrappers;
mapping(address _tokenA => IWrapper _wrapper) public tokenWrappers;
IWrapper public defaultWrapper;
mapping(string _wrapperName => IWrapper _wrapper) public wrapperRegistry;

modifier checkWrapper(string memory _wrapperName) {
if (address(wrapperRegistry[_wrapperName]) == address(0)) revert WrapperNotRegistered();
_;
}

constructor() Ownable(msg.sender) {}

function registerWrapper(string memory _wrapperName, IWrapper _wrapper) public onlyOwner {
wrapperRegistry[_wrapperName] = _wrapper;

emit WrapperRegistered(_wrapperName, _wrapper);
}

/// @dev Sets wrapper for a given pair, if the pair is quoted then this price is returned first
/// @dev Order of tokens doesn't matter
/// @param _tokenA First token in the pair
/// @param _tokenB Second token in the pair
/// @param _wrapperName Name of oracle wrapper to quote
function setPairWrapper(
address _tokenA,
address _tokenB,
string memory _wrapperName
) external checkWrapper(_wrapperName) onlyOwner {
pairWrappers[_tokenA][_tokenB] = wrapperRegistry[_wrapperName];
pairWrappers[_tokenB][_tokenA] = wrapperRegistry[_wrapperName];
}

/// @dev Sets wrapper for a given token, if the token is quoted then this price is returned if no pair is set
/// @param _token token whose wrapper is set
/// @param _wrapperName Name of oracle wrapper to quote
function setTokenWrapper(address _token, string memory _wrapperName) external checkWrapper(_wrapperName) onlyOwner {
tokenWrappers[_token] = wrapperRegistry[_wrapperName];
}

/// @dev Sets default, if the token and pair aren't registered then this is quoted as a last resort
/// @param _wrapperName Name of oracle wrapper to quote
function setDefaultWrapper(string memory _wrapperName) external checkWrapper(_wrapperName) onlyOwner {
defaultWrapper = wrapperRegistry[_wrapperName];
}

/// @dev Queries on-chain price data through a shared interface
/// @dev Pairs have priority over tokens and tokens over the default, which is the last resort
/// @param _tokenIn Token to be 'swapped' out of
/// @param _amountIn Amount of input token
/// @param _tokenOut Token to be 'swapped' into
function getAmountOut(
address _tokenIn,
uint256 _amountIn,
address _tokenOut
) public view returns (uint256 _amountOut) {
if (address(pairWrappers[_tokenIn][_tokenOut]) != address(0)) {
return pairWrappers[_tokenIn][_tokenOut].getAmountOut(_tokenIn, _amountIn, _tokenOut);
} else if (address(tokenWrappers[_tokenIn]) != address(0)) {
return tokenWrappers[_tokenIn].getAmountOut(_tokenIn, _amountIn, _tokenOut);
} else if (address(defaultWrapper) != address(0)) {
return defaultWrapper.getAmountOut(_tokenIn, _amountIn, _tokenOut);
}

revert NoWrapperSet();
}
}
37 changes: 37 additions & 0 deletions solidity/contracts/Swapper/Keeper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
pragma solidity ^0.8.19;

import {IKeep3rV2} from '../../interfaces/Swapper/IKeep3rV2.sol';
import {IWETH} from '../../interfaces/Swapper/IWETH.sol';

contract Keeper {
error KeeperNotValid();
error JobNotReady();

uint256 lastWorked;
address keep3r = 0xdc02981c9C062d48a9bD54adBf51b816623dcc6E;
uint256 constant TEN_MINUTES = 600;

IWETH constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);

modifier validateAndPayKeeper(address _keeper) {
if (!IKeep3rV2(keep3r).isKeeper(_keeper)) revert KeeperNotValid();
_;
IKeep3rV2(keep3r).directTokenPayment(address(WETH), _keeper, 1e17);
}

function work() external validateAndPayKeeper(msg.sender) {
if (!workable()) {
revert JobNotReady();
}

lastWorked = block.timestamp;

swap();
}

function workable() public view returns (bool _workable) {
return block.timestamp >= lastWorked + TEN_MINUTES;
}

function swap() public virtual {}
}
89 changes: 89 additions & 0 deletions solidity/contracts/Swapper/Swapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
pragma solidity ^0.8.19;

import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {ISwapper} from '../../interfaces/Swapper/ISwapper.sol';

/// @title Swapper contract
/// @author 0xdeo
/// @notice Allows friends to pool their tokens and make a swap
/// @notice Requires a trusted third party to provide initial liquidity
contract Swapper is ISwapper, Ownable {
using SafeERC20 for IERC20;

IERC20 fromToken;
IERC20 toToken;

uint256 netLiquidity;

mapping(uint256 => Swap) public swaps;
uint256 swapId;

constructor(address _fromToken, address _toToken) Ownable(msg.sender) {
fromToken = IERC20(_fromToken);
toToken = IERC20(_toToken);
}

/// @notice Can only provide tokens when swap hasn't been done yet
/// @notice Cannot provide tokens if fromToken balance will exceed contract's toToken balance
/// @notice User's deposit gets recorded in latest epoch, and the swapId recording his deposit is then returned
/// @param _amount Amount of fromTokens to provide
function provide(uint256 _amount) public returns (uint256) {
if (netLiquidity < _amount) {
revert InsufficientLiquidity();
}

swaps[swapId].balances[msg.sender] += _amount;
netLiquidity -= _amount;

fromToken.safeTransferFrom(msg.sender, address(this), _amount);

emit Provide(msg.sender, _amount, swapId);
return swapId;
}

/// @notice Initiates swap by sealing off further deposits and allowing withdrawals of toTokens, for that swapId's epoch
/// @notice Sends any deposited fromTokens to the owner of the contract who provided initial liquidity
function swap() public onlyOwner {
swaps[swapId].swapped = true;
swapId++;

fromToken.safeTransfer(owner(), fromToken.balanceOf(address(this)));

emit Swapped(swapId - 1);
}

/// @notice User must have nonzero balance
/// @notice If swap has been initiated, he receives toTokens in 1:1 ratio to his deposit
/// @notice Otherwise he is refunded the full amount he deposited.
/// @param _swapId ID designating epoch user's deposit belonged to
function withdraw(uint256 _swapId) public {
uint256 balance = swaps[_swapId].balances[msg.sender];
swaps[_swapId].balances[msg.sender] = 0;

if (balance == 0) {
revert ZeroBalance();
}

bool _refunded;
if (swaps[_swapId].swapped) {
toToken.safeTransfer(msg.sender, balance);
} else {
netLiquidity += balance;
_refunded = true;
fromToken.safeTransfer(msg.sender, balance);
}

emit Withdraw(msg.sender, balance, _swapId, _refunded);
}

/// @notice Can only provide tokens when swap hasn't been done yet
/// @param _liquidity amount of toTokens to provide as liquidity
function provideLiquidity(uint256 _liquidity) public onlyOwner {
netLiquidity += _liquidity;
toToken.safeTransferFrom(msg.sender, address(this), _liquidity);

emit LiquidityAdded(_liquidity);
}
}
93 changes: 93 additions & 0 deletions solidity/contracts/Swapper/SwapperV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
pragma solidity ^0.8.19;

import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {IUniswapV2Router01} from '@uniswap/periphery/interfaces/IUniswapV2Router01.sol';
import {IUniswapV2Pair} from '@uniswap/core/interfaces/IUniswapV2Pair.sol';
import {ISwapperV2} from '../../interfaces/Swapper/ISwapperV2.sol';

/// @title Swapper contract V2
/// @author 0xdeo
/// @notice Allows friends to pool their tokens and make a swap
/// @notice Routes swaps thru Uniswap
contract SwapperV2 is ISwapperV2, Ownable {
using SafeERC20 for IERC20;

IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
IERC20 constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);

IUniswapV2Router01 constant router = IUniswapV2Router01(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
IUniswapV2Pair constant pair = IUniswapV2Pair(0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11);

mapping(uint256 => Swap) public swaps;
uint256 swapId;

constructor() Ownable(msg.sender) {}

/// @notice Can only provide tokens when swap hasn't been done yet
/// @notice Cannot provide tokens if fromToken balance will exceed contract's toToken balance
/// @notice User's deposit gets recorded in latest epoch, and the swapId recording his deposit is then returned
function provide() public payable returns (uint256) {
swaps[swapId].balances[msg.sender] += msg.value;

emit Provide(msg.sender, msg.value, swapId);
return swapId;
}

/// @notice Initiates swap by sealing off further deposits and allowing withdrawals of toTokens, for that swapId's epoch
/// @notice Sends any deposited fromTokens to the owner of the contract who provided initial liquidity
function swap() public onlyOwner {
address[] memory _path = new address[](2);
_path[0] = address(WETH);
_path[1] = address(DAI);
(uint256 _reserve0, uint256 _reserve1,) = pair.getReserves();
uint256 _amountOutMin;
uint256 _amountOut = DAI.balanceOf(address(this));
uint256 _amountIn = address(this).balance;

swaps[swapId].swapped = true;
swapId++;

if (address(WETH) == pair.token0()) {
_amountOutMin = router.getAmountOut(address(this).balance, _reserve0, _reserve1);
} else {
_amountOutMin = router.getAmountOut(address(this).balance, _reserve1, _reserve0);
}

// 1% slippage to prevent frontrunning
_amountOut = router.swapExactETHForTokens{value: address(this).balance}(
_amountOutMin * 99 / 100, _path, address(this), block.timestamp
)[1];

//_amountOut = DAI.balanceOf(address(this)) - _amountOut;
swaps[swapId - 1].totalOut = _amountOut;
swaps[swapId - 1].totalIn = _amountIn;

emit Swapped(swapId - 1);
}

/// @notice User must have nonzero balance
/// @notice If swap has been initiated, he receives toTokens in 1:1 ratio to his deposit
/// @notice Otherwise he is refunded the full amount he deposited.
/// @param _swapId ID designating epoch user's deposit belonged to
function withdraw(uint256 _swapId) public {
uint256 balance = swaps[_swapId].balances[msg.sender];
swaps[_swapId].balances[msg.sender] = 0;

if (balance == 0) {
revert ZeroBalance();
}

bool _refunded;
if (swaps[_swapId].swapped) {
balance = (balance * swaps[_swapId].totalOut) / swaps[_swapId].totalIn;
DAI.safeTransfer(msg.sender, balance);
} else {
_refunded = true;
payable(msg.sender).transfer(balance);
}

emit Withdraw(msg.sender, balance, _swapId, _refunded);
}
}
Loading
Loading