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

Univ2 LP Deposit After Univ3 Trade #18

Open
wants to merge 19 commits into
base: univ2-lp-deposit
Choose a base branch
from
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
45 changes: 24 additions & 21 deletions src/KilnUniV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

pragma solidity ^0.8.14;

import {KilnBase, GemLike} from "./KilnBase.sol";
import {TwapProduct} from "./uniV3/TwapProduct.sol";
import {KilnBase, GemLike} from "src/KilnBase.sol";
import {IQuoter} from "src/quoters/IQuoter.sol";

// https://github.com/Uniswap/v3-periphery/blob/b06959dd01f5999aa93e1dc530fe573c7bb295f6/contracts/SwapRouter.sol
interface SwapRouterLike {
Expand All @@ -35,20 +35,18 @@ struct ExactInputParams {
uint256 amountOutMinimum;
}

contract KilnUniV3 is KilnBase, TwapProduct {
uint256 public scope; // [Seconds] Time period for TWAP calculations
uint256 public yen; // [WAD] Relative multiplier of the TWAP's price to insist on
bytes public path; // ABI-encoded UniV3 compatible path
contract KilnUniV3 is KilnBase {
uint256 public yen; // [WAD] Relative multiplier of the reference price to insist on in the UniV3 trade.
// For example: 0.98 * WAD allows 2% worse price than the reference.
bytes public path; // ABI-encoded UniV3 compatible path
address public quoter;

address public immutable uniV3Router;
address public immutable receiver;

event File(bytes32 indexed what, address data);
event File(bytes32 indexed what, bytes data);

// @notice initialize a Uniswap V3 routing path contract
// @dev TWAP-relative trading is enabled by default. With the initial values, fire will
// perform the trade only when the amount of tokens received is equal or better than
// the 1 hour average price.
// @param _sell the contract address of the token that will be sold
// @param _buy the contract address of the token that will be purchased
// @param _uniV3Router the address of the current Uniswap V3 swap router
Expand All @@ -60,17 +58,25 @@ contract KilnUniV3 is KilnBase, TwapProduct {
address _receiver
)
KilnBase(_sell, _buy)
TwapProduct(SwapRouterLike(_uniV3Router).factory())
{
uniV3Router = _uniV3Router;
receiver = _receiver;

scope = 1 hours;
yen = WAD;
}

uint256 constant WAD = 10 ** 18;

function _max(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = x >= y ? x : y;
}

function file(bytes32 what, address data) public virtual auth {
if (what == "quoter") quoter = data;
else revert("KilnUniV3/file-unrecognized-param");
emit File(what, data);
}

/**
@dev Auth'ed function to update path value
@param what Tag of value to update
Expand All @@ -83,19 +89,14 @@ contract KilnUniV3 is KilnBase, TwapProduct {
}

/**
@dev Auth'ed function to update yen, scope, or base contract derived values
@dev Auth'ed function to update yen or base contract derived values
Warning - setting `yen` as 0 or another low value highly increases the susceptibility to oracle manipulation attacks
Warning - a low `scope` increases the susceptibility to oracle manipulation attacks
@param what Tag of value to update
@param data Value to update
*/
function file(bytes32 what, uint256 data) public override auth {
if (what == "yen") yen = data;
else if (what == "scope") {
require(data > 0, "KilnUniV3/zero-scope");
require(data <= uint32(type(int32).max), "KilnUniV3/scope-overflow");
scope = data;
} else {
if (what == "yen") yen = data;
else {
super.file(what, data);
return;
}
Expand All @@ -108,7 +109,9 @@ contract KilnUniV3 is KilnBase, TwapProduct {
bytes memory _path = path;
uint256 _yen = yen;

uint256 amountMin = (_yen != 0) ? quote(_path, amount, uint32(scope)) * _yen / WAD : 0;
uint256 amountMin = (_yen != 0) ?
IQuoter(quoter).quote(sell, buy, amount) * _yen / WAD :
0;

ExactInputParams memory params = ExactInputParams({
path: _path,
Expand Down
77 changes: 52 additions & 25 deletions src/KilnUniV3.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
pragma solidity ^0.8.14;

import "forge-std/Test.sol";
import "./KilnUniV3.sol";
import "src/KilnUniV3.sol";
import "src/quoters/MaxAggregator.sol";
import "src/quoters/QuoterTwapProduct.sol";

interface TestGem {
function totalSupply() external view returns (uint256);
}

// https://github.com/Uniswap/v3-periphery/blob/v1.0.0/contracts/lens/Quoter.sol#L106-L122
interface Quoter {
interface Univ3Quoter {
function quoteExactInput(
bytes calldata path,
uint256 amountIn
Expand All @@ -33,9 +35,17 @@ interface Quoter {

contract User {}

contract HighAmountQuoter is IQuoter {
function quote(address, address, uint256 amountIn) external pure returns (uint256 amountOut) {
return amountIn; // MKR / DAI = 1, much higher out amount than usual
}
}

contract KilnTest is Test {
KilnUniV3 kiln;
Quoter quoter;
MaxAggregator aggregator;
QuoterTwapProduct qtwap;
Univ3Quoter quoter;
User user;

bytes path;
Expand All @@ -51,20 +61,29 @@ contract KilnTest is Test {
address constant QUOTER = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6;
address constant FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984;

event File(bytes32 indexed what, address data);
event File(bytes32 indexed what, bytes data);
event File(bytes32 indexed what, uint256 data);
event AddQuoter(address indexed quoter);
event RemoveQuoter(uint256 indexed index, address indexed quoter);

function setUp() public {
user = new User();
path = abi.encodePacked(DAI, uint24(100), USDC, uint24(500), WETH, uint24(3000), MKR);

kiln = new KilnUniV3(DAI, MKR, ROUTER, address(user));
quoter = Quoter(QUOTER);
aggregator = new MaxAggregator();
quoter = Univ3Quoter(QUOTER);

kiln.file("lot", 50_000 * WAD);
kiln.file("hop", 6 hours);
kiln.file("path", path);

qtwap = new QuoterTwapProduct(FACTORY);
qtwap.file("path", path);
aggregator.addQuoter(address(qtwap));
kiln.file("quoter", address(aggregator));

kiln.file("yen", 50 * WAD / 100); // Insist on very little on default
}

Expand Down Expand Up @@ -98,6 +117,13 @@ contract KilnTest is Test {
SwapRouterLike(kiln.uniV3Router()).exactInput(params);
}

function testFileQuoter() public {
vm.expectEmit(true, true, false, false);
emit File(bytes32("quoter"), address(314));
kiln.file("quoter", address(314));
assertEq(kiln.quoter(), address(314));
}

function testFilePath() public {
path = abi.encodePacked(DAI, uint24(100), USDC);
vm.expectEmit(true, true, false, false);
Expand All @@ -113,21 +139,9 @@ contract KilnTest is Test {
assertEq(kiln.yen(), 42);
}

function testFileScope() public {
vm.expectEmit(true, true, false, false);
emit File(bytes32("scope"), 314);
kiln.file("scope", 314);
assertEq(kiln.scope(), 314);
}

function testFileZeroScope() public {
vm.expectRevert("KilnUniV3/zero-scope");
kiln.file("scope", 0);
}

function testFileScopeTooLarge() public {
vm.expectRevert("KilnUniV3/scope-overflow");
kiln.file("scope", uint32(type(int32).max) + 1);
function testFileAddressUnrecognized() public {
vm.expectRevert("KilnUniV3/file-unrecognized-param");
kiln.file("nonsense", address(314));
}

function testFileBytesUnrecognized() public {
Expand All @@ -140,6 +154,12 @@ contract KilnTest is Test {
kiln.file("nonsense", 23);
}

function testFileQuoterNonAuthed() public {
vm.startPrank(address(123));
vm.expectRevert("KilnBase/not-authorized");
kiln.file("quoter", address(314));
}

function testFilePathNonAuthed() public {
vm.startPrank(address(123));
vm.expectRevert("KilnBase/not-authorized");
Expand All @@ -152,10 +172,17 @@ contract KilnTest is Test {
kiln.file("yen", 42);
}

function testFileScopeNonAuthed() public {
vm.startPrank(address(123));
vm.expectRevert("KilnBase/not-authorized");
kiln.file("scope", 413);
function testMultipleQuoters() public {
// Add a quoter with a higher amount than usual to act as reference
HighAmountQuoter q2 = new HighAmountQuoter();
aggregator.addQuoter(address(q2));

// Permissive values
kiln.file("yen", 50 * WAD / 100);

deal(DAI, address(kiln), 50_000 * WAD);
vm.expectRevert("Too little received");
kiln.fire();
}

function testFireYenMuchLessThanTwap() public {
Expand Down Expand Up @@ -268,8 +295,8 @@ contract KilnTest is Test {
mintDai(address(kiln), 1_000_000 * WAD);

kiln.file("hop", 0 hours); // for convenience allow firing right away
kiln.file("scope", 1 hours);
kiln.file("yen", 120 * WAD / 100); // only swap if price rose by 20% vs twap
qtwap.file("scope", 1 hours);

uint256 mkrBefore = GemLike(MKR).balanceOf(address(this));

Expand Down Expand Up @@ -304,8 +331,8 @@ contract KilnTest is Test {
mintDai(address(kiln), 1_000_000 * WAD);

kiln.file("hop", 0 hours); // for convenience allow firing right away
kiln.file("scope", 1 hours);
kiln.file("yen", 80 * WAD / 100); // allow swap even if price fell by 20% vs twap
qtwap.file("scope", 1 hours);

// make sure twap measures regular MKR out amount at the beginning of the hour (by making small swap)
vm.roll(block.number + 1);
Expand Down
Loading