Skip to content

Commit

Permalink
feat: Check and update the spread to avoid atomic sandwich attack (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
fb-alexcq authored Apr 10, 2024
1 parent ef11dff commit 8b086a3
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ jobs:
- name: Run TypeScript/Waffle tests
run: yarn test
- name: Run Solidity/Forge tests
run: forge test
run: forge test --fork-url https://rpc.ankr.com/eth
69 changes: 60 additions & 9 deletions contracts/wooracle/WooracleV2_2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ contract WooracleV2_2 is Ownable, IWooracleV2_2 {
/// @param _base the baseToken address
/// @param _price the new prices for the base token
function postPrice(address _base, uint128 _price) external onlyAdmin {
// NOTE: update spread before setting a new price
_updateSpreadForNewPrice(_base, _price);
infos[_base].price = _price;
if (msg.sender != wooPP) {
timestamp = block.timestamp;
Expand All @@ -178,6 +180,8 @@ contract WooracleV2_2 is Ownable, IWooracleV2_2 {
uint128 _price,
uint256 _ts
) external onlyAdmin {
// NOTE: update spread before setting a new price
_updateSpreadForNewPrice(_base, _price);
infos[_base].price = _price;
timestamp = _ts;
}
Expand All @@ -193,10 +197,10 @@ contract WooracleV2_2 is Ownable, IWooracleV2_2 {
uint256 length = _bases.length;
require(length == _prices.length, "WooracleV2_2: length_INVALID");

unchecked {
for (uint256 i = 0; i < length; i++) {
infos[_bases[i]].price = _prices[i];
}
for (uint256 i = 0; i < length; i++) {
// NOTE: update spread before setting a new price
_updateSpreadForNewPrice(_bases[i], _prices[i]);
infos[_bases[i]].price = _prices[i];
}

timestamp = _ts;
Expand Down Expand Up @@ -247,10 +251,8 @@ contract WooracleV2_2 is Ownable, IWooracleV2_2 {
uint256 _ts
) external onlyAdmin {
uint256 length = _bases.length;
unchecked {
for (uint256 i = 0; i < length; i++) {
_setState(_bases[i], _prices[i], _spreads[i], _coeffs[i]);
}
for (uint256 i = 0; i < length; i++) {
_setState(_bases[i], _prices[i], _spreads[i], _coeffs[i]);
}
timestamp = _ts;
}
Expand Down Expand Up @@ -328,15 +330,61 @@ contract WooracleV2_2 is Ownable, IWooracleV2_2 {

/* ----- Internal Functions ----- */

function _updateSpreadForNewPrice(address _base, uint128 _price) internal {
uint64 preS = infos[_base].spread;
uint128 preP = infos[_base].price;
if (preP == 0 || _price == 0 || preS >= 1e18) {
// previous price or current price is 0, no action is needed
return;
}

uint256 maxP = _price >= preP ? _price : preP;
uint256 minP = _price <= preP ? _price : preP;
uint256 antiS = (uint256(1e18) * 1e18 * minP) / maxP / (uint256(1e18) - preS);
if (antiS < 1e18) {
uint64 newS = uint64(1e18 - antiS);
if (newS > preS) {
infos[_base].spread = newS;
}
}
}

function _updateSpreadForNewPrice(
address _base,
uint128 _price,
uint64 _spread
) internal {
require(_spread < 1e18, "!_spread");

uint64 preS = infos[_base].spread;
uint128 preP = infos[_base].price;
if (preP == 0 || _price == 0 || preS >= 1e18) {
// previous price or current price is 0, just use _spread
infos[_base].spread = _spread;
return;
}

uint256 maxP = _price >= preP ? _price : preP;
uint256 minP = _price <= preP ? _price : preP;
uint256 antiS = (uint256(1e18) * 1e18 * minP) / maxP / (uint256(1e18) - preS);
if (antiS < 1e18) {
uint64 newS = uint64(1e18 - antiS);
infos[_base].spread = newS > _spread ? newS : _spread;
} else {
infos[_base].spread = _spread;
}
}

function _setState(
address _base,
uint128 _price,
uint64 _spread,
uint64 _coeff
) internal {
TokenInfo storage info = infos[_base];
// NOTE: update spread before setting a new price
_updateSpreadForNewPrice(_base, _price, _spread);
info.price = _price;
info.spread = _spread;
info.coeff = _coeff;
}

Expand Down Expand Up @@ -429,6 +477,9 @@ contract WooracleV2_2 is Ownable, IWooracleV2_2 {
for (uint256 i = 0; i < len; ++i) {
base = getBase(uint8(bytes1(_input[1 + i * 5:1 + i * 5 + 1])));
p = _decodePrice(uint32(bytes4(_input[1 + i * 5 + 1:1 + i * 5 + 5])));

// NOTE: update spread before setting a new price
_updateSpreadForNewPrice(base, p);
infos[base].price = p;
}

Expand Down
27 changes: 0 additions & 27 deletions test/foundry/Greeter.t.sol

This file was deleted.

110 changes: 110 additions & 0 deletions test/foundry/TestWooracleV2_2.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// import {Greeter} from "../../contracts/Greeter.sol";
import {TestHelpers} from "./TestHelpers.sol";

import {WooPPV2} from "../../contracts/WooPPV2.sol";
import {WooRouterV2} from "../../contracts/WooRouterV2.sol";
import {WooracleV2_2} from "../../contracts/wooracle/WooracleV2_2.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import "forge-std/console.sol";

contract TestWooracleV2_2 is TestHelpers {

WooPPV2 public pool;
WooRouterV2 public router;
WooracleV2_2 public oracle;

address private constant ADMIN = address(1);
address private constant ATTACKER = address(2);
address private constant FEE_ADDR = address(4);

// mainnet
address private constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address private constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // QUOTE TOKEN
address private constant WOO = 0x4691937a7508860F876c9c0a2a617E7d9E945D4B;
address private constant USDC_USD_ORACLE = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6;
address private constant ETH_USD_ORACLE = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419;

uint128 private constant MAX_NOTIONAL_USDC = 5000_000 * 1e18;
uint128 private constant MAX_GAMMA = type(uint128).max;

// deployed addresses

function setUp() public {
vm.startPrank(ADMIN);
pool = new WooPPV2(USDC);
router = new WooRouterV2(WETH, address(pool));
oracle = new WooracleV2_2();

oracle.setQuoteToken(USDC, USDC_USD_ORACLE);
oracle.setCLOracle(WETH, ETH_USD_ORACLE, true);
oracle.setWooPP(address(pool));

oracle.postState(WETH, 346288977288, 363000000000000, 1000000000);
oracle.setGuardian(ADMIN, true);
oracle.setRange(WOO, 9000, 110000000);
oracle.setAdmin(address(pool), true);

pool.setWooracle(address(oracle));
pool.setTokenInfo(WETH, 0, MAX_GAMMA, MAX_NOTIONAL_USDC);
pool.setTokenInfo(WOO, 0, MAX_GAMMA, MAX_NOTIONAL_USDC);
pool.setFeeAddr(FEE_ADDR);
vm.stopPrank();
}

// forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/KyziwbHHJ7gO9OuNFklgxHB74nF8jUWI --match-contract TestWooracleV2_2 -vvvv
function test_Wooracle() public {
vm.startPrank(ADMIN);

oracle.woState(WETH);
// (346288977288, 363000000000000, 1000000000, true)

oracle.postState(WETH, 349833999632, 655000000000000, 1000000000);
oracle.woState(WETH);

//post (349833999632, 655000000000000, 1000000000)
// (349833999632, 9773989383877141, 1000000000, true)

oracle.postState(WETH, 348921000000, 566000000000000, 1000000000);
oracle.woState(WETH);

oracle.postState(WETH, 350021000000, 111000000000000, 1000000000);
oracle.woState(WETH);

oracle.postState(WETH, 351121000000, 111000000000000, 1000000000);
oracle.woState(WETH);

oracle.postState(WETH, 345420000000, 861000000000000, 1000000000);
oracle.postState(WBTC, 6846630000000, 650000000000000, 1000000000);
oracle.woState(WETH);
oracle.woState(WBTC);

oracle.postState(WETH, 345400000000, 0, 1000000000);
oracle.woState(WETH);

oracle.postState(WETH, 345820000000, 861000000000000, 1000000000);
oracle.postState(WBTC, 6859100000000, 848000000000000, 1000000000);
oracle.woState(WETH);
oracle.woState(WBTC);

oracle.postState(WETH, 345820000000, 111000000000000, 1000000000);
oracle.woState(WETH);

oracle.postState(WETH, 345820000000, 0, 0);
oracle.woState(WETH);

oracle.postState(WETH, 345830000000, 0, 0);
oracle.woState(WETH);

oracle.postState(WETH, 345820000000, 111000000000000, 1000000000);
oracle.woState(WETH);

vm.stopPrank();
}

}

0 comments on commit 8b086a3

Please sign in to comment.