From ec71bcd2875ad7c819d77dfd88c455895be5f97c Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Fri, 7 Jun 2024 11:22:22 +0200 Subject: [PATCH] added price oracle --- onchain/src/BaseLineLP.sol | 21 +++++++++++++-- onchain/src/Stake.sol | 51 +++++++++++++++++++++++++++++++---- onchain/test/BaseLineLP.t.sol | 18 +++++++++++++ 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/onchain/src/BaseLineLP.sol b/onchain/src/BaseLineLP.sol index e7d9d5a..99703bb 100644 --- a/onchain/src/BaseLineLP.sol +++ b/onchain/src/BaseLineLP.sol @@ -24,6 +24,7 @@ contract BaseLineLP { int24 constant TICK_SPACING = 200; int24 constant ANCHOR_SPACING = 5 * TICK_SPACING; int24 constant DISCOVERY_SPACING = 11000; + int24 constant MAX_TICK_DEVIATION = 50; // default fee of 1% uint24 constant FEE = uint24(10_000); // uint256 constant FLOOR = 0; @@ -345,12 +346,27 @@ contract BaseLineLP { } } + + function _checkPriceStability(int24 currentTick) internal view returns (bool) { + uint32 timeInterval = 300; // 5 minutes in seconds + uint32[] memory secondsAgo = new uint32[](2); + secondsAgo[0] = timeInterval; // 5 minutes ago + secondsAgo[1] = 0; // current block timestamp + + (int56[] memory tickCumulatives,) = pool.observe(secondsAgo); + int56 tickCumulativeDiff = tickCumulatives[1] - tickCumulatives[0]; + int24 averageTick = int24(tickCumulativeDiff / int56(int32(timeInterval))); + + return (currentTick >= averageTick - MAX_TICK_DEVIATION && currentTick <= averageTick + MAX_TICK_DEVIATION); + } + // call this function when price has moved up 15% function shift() external { require(positions[Stage.ANCHOR].liquidity > 0, "Not initialized"); // Fetch the current tick from the Uniswap V3 pool (uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0(); - // TODO: check slippage with oracle + // check slippage with oracle + _checkPriceStability(currentTick); // ## check price moved up { @@ -406,7 +422,8 @@ contract BaseLineLP { function slide() external { // Fetch the current tick from the Uniswap V3 pool (uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0(); - // TODO: check slippage with oracle + // check slippage with oracle + _checkPriceStability(currentTick); // ## check price moved down if (positions[Stage.ANCHOR].liquidity > 0) { diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index 03dbec7..a63b0fd 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -11,6 +11,7 @@ import "./interfaces/IStake.sol"; import "./Harb.sol"; error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake); +error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStake, uint256 smallestShare); contract Stake is IStake { using Math for uint256; @@ -32,6 +33,7 @@ contract Stake is IStake { event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 share, uint32 creationTime, uint32 taxRate); event TaxPaid(uint256 indexed positionId, address indexed owner, uint256 taxAmount); event PositionRemoved(uint256 indexed positionId, uint256 share, uint32 lastTaxTime); + event PositionShrunk(uint256 indexed positionId, uint256 share, uint32 lastTaxTime, uint256 sharesTaken); struct StakingPosition { uint256 share; @@ -93,6 +95,8 @@ contract Stake is IStake { return snatch(assets, receiver, taxRate, positionsToSnatch); } + + /** * TODO: deal with metatransactions: While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct @@ -111,8 +115,10 @@ contract Stake is IStake { } // TODO: check that position size is multiple of minStake + uint256 smallestPositionShare = totalSupply; + // run through all suggested positions to snatch - for (uint256 i = 0; i < positionsToSnatch.length; i++) { + for (uint256 i = 0; i < positionsToSnatch.length - 1; i++) { StakingPosition storage pos = positions[positionsToSnatch[i]]; if (pos.creationTime == 0) { //TODO: @@ -122,20 +128,48 @@ contract Stake is IStake { if (taxRate <= pos.taxRate) { revert TaxTooLow(receiver, taxRate, pos.taxRate, i); } + if (pos.share < smallestPositionShare) { + smallestPositionShare = pos.share; + } // dissolve position // TODO: what if someone calls payTax and exitPosition in the same transaction? _payTax(positionsToSnatch[i], pos, 0); _exitPosition(positionsToSnatch[i], pos); - // TODO: exit positions partially, if needed - // TODO: avoid greeving where more positions are freed than needed. } - // try to make a new position in the free space and hope it is big enough uint256 availableStake = authorizedStake() - outstandingStake; + + // handle last position + if (positionsToSnatch.length > 0) { + uint256 index = positionsToSnatch.length - 1; + StakingPosition storage pos = positions[positionsToSnatch[index]]; + if (pos.creationTime == 0) { + //TODO: + revert PositionNotFound(); + } + // check that tax lower + if (taxRate <= pos.taxRate) { + revert TaxTooLow(receiver, taxRate, pos.taxRate, index); + } + if (pos.share < smallestPositionShare) { + smallestPositionShare = pos.share; + } + // dissolve position + _payTax(positionsToSnatch[index], pos, 0); + uint256 lastBitNeeded = sharesWanted - availableStake; + _shrinkPosition(positionsToSnatch[index], pos, lastBitNeeded); + availableStake += lastBitNeeded; + } + if (sharesWanted > availableStake) { revert ExceededAvailableStake(receiver, sharesWanted, availableStake); } + // avoid greeving where more positions are freed than needed. + if (availableStake - sharesWanted > smallestPositionShare) { + revert TooMuchSnatch(receiver, sharesWanted, availableStake, smallestPositionShare); + } + // transfer SafeERC20.safeTransferFrom(tokenContract, msg.sender, address(this), assets); @@ -210,7 +244,6 @@ contract Stake is IStake { pos.lastTaxTime = uint32(block.timestamp); } else { // if nothing left over, liquidate position - // TODO: emit event outstandingStake -= pos.share; emit PositionRemoved(positionID, pos.share, pos.lastTaxTime); delete pos.owner; @@ -227,4 +260,12 @@ contract Stake is IStake { delete pos.creationTime; SafeERC20.safeTransfer(tokenContract, owner, assets); } + + function _shrinkPosition(uint256 positionId, StakingPosition storage pos, uint256 sharesToTake) private { + require (sharesToTake > pos.share, "position too small"); + pos.share -= sharesToTake; + uint256 assets = sharesToAssets(sharesToTake, Math.Rounding.Down); + emit PositionShrunk(positionId, pos.share, pos.lastTaxTime, sharesToTake); + SafeERC20.safeTransfer(tokenContract, pos.owner, assets); + } } diff --git a/onchain/test/BaseLineLP.t.sol b/onchain/test/BaseLineLP.t.sol index 9a05e9d..b264bda 100644 --- a/onchain/test/BaseLineLP.t.sol +++ b/onchain/test/BaseLineLP.t.sol @@ -98,6 +98,10 @@ contract BaseLineLPTest is PoolSerializer { vm.expectRevert(); lm.shift(); + // have some time pass to record prices in uni oracle + uint256 timeBefore = block.timestamp; + vm.warp(timeBefore + (60 * 60 * 5)); + // Setup of liquidity lm.slide(); appendPossitions(lm, pool, token0isWeth); @@ -114,6 +118,10 @@ contract BaseLineLPTest is PoolSerializer { token0isWeth ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, abi.encode(account, int256(0.5 ether), true) ); + // have some time pass to record prices in uni oracle + timeBefore = block.timestamp; + vm.warp(timeBefore + (60 * 60 * 5)); + lm.shift(); appendPossitions(lm, pool, token0isWeth); @@ -126,6 +134,10 @@ contract BaseLineLPTest is PoolSerializer { token0isWeth ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, abi.encode(account, int256(3 ether), true) ); + // have some time pass to record prices in uni oracle + timeBefore = block.timestamp; + vm.warp(timeBefore + (60 * 60 * 5)); + lm.shift(); appendPossitions(lm, pool, token0isWeth); @@ -141,6 +153,9 @@ contract BaseLineLPTest is PoolSerializer { !token0isWeth ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, abi.encode(account, int256(300000 ether), false) ); + // have some time pass to record prices in uni oracle + timeBefore = block.timestamp; + vm.warp(timeBefore + (60 * 60 * 5)); lm.slide(); appendPossitions(lm, pool, token0isWeth); @@ -157,6 +172,9 @@ contract BaseLineLPTest is PoolSerializer { !token0isWeth ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, abi.encode(account, int256(3600000 ether), false) ); + // have some time pass to record prices in uni oracle + timeBefore = block.timestamp; + vm.warp(timeBefore + (60 * 60 * 5)); // add to CSV lm.slide();