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

feat: price oracle validation #318

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions snapshots/PriceOracleValidationTest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"testPriceOracleValidationSucceeds": "176008"
}
2 changes: 1 addition & 1 deletion src/interfaces/IValidationCallback.sol
Original file line number Diff line number Diff line change
@@ -8,5 +8,5 @@ interface IValidationCallback {
/// @notice Called by the reactor for custom validation of an order. Will revert if validation fails
/// @param filler The filler of the order
/// @param resolvedOrder The resolved order to fill
function validate(address filler, ResolvedOrder calldata resolvedOrder) external view;
function validate(address filler, ResolvedOrder calldata resolvedOrder) external;
}
2 changes: 1 addition & 1 deletion src/lib/ResolvedOrderLib.sol
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ library ResolvedOrderLib {

/// @notice Validates a resolved order, reverting if invalid
/// @param filler The filler of the order
function validate(ResolvedOrder memory resolvedOrder, address filler) internal view {
function validate(ResolvedOrder memory resolvedOrder, address filler) internal {
if (address(this) != address(resolvedOrder.info.reactor)) {
revert InvalidReactor();
}
32 changes: 32 additions & 0 deletions src/sample-validation-contracts/PriceOracleValidation.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {IValidationCallback} from "../interfaces/IValidationCallback.sol";
import {ResolvedOrder} from "../base/ReactorStructs.sol";
import {ISwapRouter02} from "../external/ISwapRouter02.sol";

/// @notice Validation contract that checks
/// @dev uses swapRouter02
contract PriceOracleValidation is IValidationCallback {
error FailedToCallValidationContract(bytes reason);
error InsufficientOutput(uint256 minOutput, uint256 actualOutput);

function validate(address, ResolvedOrder calldata resolvedOrder) external {
(address to, bytes memory data) = abi.decode(resolvedOrder.info.additionalValidationData, (address, bytes));

// No strict interface enforced here
(bool success, bytes memory returnData) = address(to).call(data);
if (!success) {
revert FailedToCallValidationContract(returnData);
}
uint256 amountOut = abi.decode(returnData, (uint256));

uint256 totalOutputAmount;
for (uint256 i = 0; i < resolvedOrder.outputs.length; i++) {
totalOutputAmount += resolvedOrder.outputs[i].amount;
}
if (amountOut < totalOutputAmount) {
revert InsufficientOutput(amountOut, totalOutputAmount);
}
}
}
55 changes: 55 additions & 0 deletions test/integration/SwapRouter02ExecutorIntegration.t.sol
Original file line number Diff line number Diff line change
@@ -13,6 +13,9 @@ import {OutputsBuilder} from "../util/OutputsBuilder.sol";
import {PermitSignature} from "../util/PermitSignature.sol";
import {ISwapRouter02, ExactInputSingleParams} from "../../src/external/ISwapRouter02.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {PriceOracleValidation} from "../../src/sample-validation-contracts/PriceOracleValidation.sol";
import {IValidationCallback} from "../../src/interfaces/IValidationCallback.sol";
import {MockMixedRouteQuoterV1Wrapper} from "../util/mock/MockMixedRouteQuoterV1Wrapper.sol";

// This set of tests will use a mainnet fork to test integration.
contract SwapRouter02IntegrationTest is Test, PermitSignature {
@@ -27,6 +30,7 @@ contract SwapRouter02IntegrationTest is Test, PermitSignature {
address constant WHALE = 0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E;
IPermit2 constant PERMIT2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3);
uint256 constant ONE = 1000000000000000000;
address constant MIXED_ROUTE_QUOTER = 0x84E44095eeBfEC7793Cd7d5b57B7e401D7f1cA2E;

address swapper;
address swapper2;
@@ -35,6 +39,8 @@ contract SwapRouter02IntegrationTest is Test, PermitSignature {
address filler;
SwapRouter02Executor swapRouter02Executor;
DutchOrderReactor dloReactor;
IValidationCallback priceOracleValidationContract;
MockMixedRouteQuoterV1Wrapper mockMixedRouteQuoterV1Wrapper;

function setUp() public {
swapperPrivateKey = 0xbabe;
@@ -45,6 +51,8 @@ contract SwapRouter02IntegrationTest is Test, PermitSignature {
vm.createSelectFork(vm.envString("FOUNDRY_RPC_URL"), 16586505);
dloReactor = new DutchOrderReactor(PERMIT2, address(0));
swapRouter02Executor = new SwapRouter02Executor(address(this), dloReactor, address(this), SWAPROUTER02);
priceOracleValidationContract = IValidationCallback(address(new PriceOracleValidation()));
mockMixedRouteQuoterV1Wrapper = new MockMixedRouteQuoterV1Wrapper(MIXED_ROUTE_QUOTER);

// Swapper max approves permit post
vm.prank(swapper);
@@ -410,6 +418,53 @@ contract SwapRouter02IntegrationTest is Test, PermitSignature {
assertEq(address(swapRouter02Executor).balance, 319317550497372609);
}

// Same setup as test above
function testSwapWethToDaiViaV2_priceOracleValidationContract() public {
uint256 inputAmount = 2 * ONE;
address[] memory path = new address[](2);
path[0] = address(WETH);
path[1] = address(DAI);
bytes memory encodedPath = abi.encodePacked(path[0], uint24(uint256(3000)), path[1]);

bytes memory additionalValidationData = abi.encode(
address(mockMixedRouteQuoterV1Wrapper),
abi.encodeWithSelector(bytes4(keccak256("quoteExactInput(bytes,uint256)")), encodedPath, inputAmount)
);

uint256 outputAmount = 3000 * ONE;

DutchOrder memory order = DutchOrder({
info: OrderInfoBuilder.init(address(dloReactor)).withSwapper(swapper).withDeadline(block.timestamp + 100)
.withValidationContract(priceOracleValidationContract).withValidationData(additionalValidationData),
decayStartTime: block.timestamp - 100,
decayEndTime: block.timestamp + 100,
input: DutchInput(WETH, inputAmount, inputAmount),
outputs: OutputsBuilder.singleDutch(address(DAI), outputAmount, outputAmount, address(swapper))
});

address[] memory tokensToApproveForSwapRouter02 = new address[](1);
tokensToApproveForSwapRouter02[0] = address(WETH);

address[] memory tokensToApproveForReactor = new address[](1);
tokensToApproveForReactor[0] = address(DAI);
bytes[] memory multicallData = new bytes[](1);

multicallData[0] = abi.encodeWithSelector(
ISwapRouter02.swapExactTokensForTokens.selector,
inputAmount,
outputAmount,
path,
address(swapRouter02Executor)
);
swapRouter02Executor.execute(
SignedOrder(abi.encode(order), signOrder(swapperPrivateKey, address(PERMIT2), order)),
abi.encode(tokensToApproveForSwapRouter02, tokensToApproveForReactor, multicallData)
);
assertEq(WETH.balanceOf(swapper), ONE);
assertEq(DAI.balanceOf(swapper), outputAmount);
assertEq(DAI.balanceOf(address(swapRouter02Executor)), 275438458971501955836);
}

// There is 10 WETH swapRouter02Executor. Test that we can convert it to ETH
// and withdraw successfully.
function testUnwrapWETH() public {
26 changes: 26 additions & 0 deletions test/util/mock/MockMixedRouteQuoterV1Wrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

/// @notice Helper contract to call MixedRouteQuoterV1 and decode the return data
contract MockMixedRouteQuoterV1Wrapper {
address private immutable quoter;

constructor(address _quoter) {
quoter = _quoter;
}

fallback(bytes calldata data) external returns (bytes memory) {
// quoteExactInput(bytes memory path, uint256 amountIn)
if (msg.sig != 0xcdca1753) {
revert("Invalid function call");
}

(bool success, bytes memory returnData) = address(quoter).call(data);
if (!success) {
revert("Failed to call quoter");
}

(uint256 amountOut,,,) = abi.decode(returnData, (uint256, uint160[], uint32[], uint256));
return abi.encode(amountOut);
}
}
2 changes: 1 addition & 1 deletion test/util/mock/MockResolvedOrderLib.sol
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import {ResolvedOrderLib} from "../../../src/lib/ResolvedOrderLib.sol";
contract MockResolvedOrderLib {
using ResolvedOrderLib for ResolvedOrder;

function validate(ResolvedOrder memory resolvedOrder, address filler) external view {
function validate(ResolvedOrder memory resolvedOrder, address filler) external {
resolvedOrder.validate(filler);
}
}
108 changes: 108 additions & 0 deletions test/validation-contracts/PriceOracleValidation.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {Test} from "forge-std/Test.sol";
import {DeployPermit2} from "../util/DeployPermit2.sol";
import {DutchOrderReactor, DutchOrder, DutchInput} from "../../src/reactors/DutchOrderReactor.sol";
import {OrderInfo, SignedOrder} from "../../src/base/ReactorStructs.sol";
import {OrderInfoBuilder} from "../util/OrderInfoBuilder.sol";
import {MockERC20} from "../util/mock/MockERC20.sol";
import {DutchOrder, DutchOrderLib} from "../../src/lib/DutchOrderLib.sol";
import {OutputsBuilder} from "../util/OutputsBuilder.sol";
import {MockFillContract} from "../util/mock/MockFillContract.sol";
import {PermitSignature} from "../util/PermitSignature.sol";
import {PriceOracleValidation} from "../../src/sample-validation-contracts/PriceOracleValidation.sol";
import {ResolvedOrderLib} from "../../src/lib/ResolvedOrderLib.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";

contract MockPriceOracle {
fallback(bytes calldata data) external returns (bytes memory) {
return data;
}
}

contract PriceOracleValidationTest is Test, PermitSignature, DeployPermit2 {
using OrderInfoBuilder for OrderInfo;
using DutchOrderLib for DutchOrder;

address constant PROTOCOL_FEE_OWNER = address(1);

MockFillContract fillContract;
MockERC20 tokenIn;
MockERC20 tokenOut;
uint256 swapperPrivateKey;
address swapper;
DutchOrderReactor reactor;
IPermit2 permit2;
PriceOracleValidation priceOracleValidation;
MockPriceOracle mockPriceOracle;

function setUp() public {
tokenIn = new MockERC20("Input", "IN", 18);
tokenOut = new MockERC20("Output", "OUT", 18);
swapperPrivateKey = 0x12341234;
swapper = vm.addr(swapperPrivateKey);
permit2 = IPermit2(deployPermit2());
reactor = new DutchOrderReactor(permit2, PROTOCOL_FEE_OWNER);
fillContract = new MockFillContract(address(reactor));
mockPriceOracle = new MockPriceOracle();
priceOracleValidation = new PriceOracleValidation();
}

// Test price oracle validation contract succeeds
function testPriceOracleValidationSucceeds() public {
uint256 inputAmount = 10 ** 18;
uint256 outputAmount = 2 * inputAmount;

tokenIn.mint(address(swapper), inputAmount);
tokenOut.mint(address(fillContract), outputAmount);
tokenIn.forceApprove(swapper, address(permit2), type(uint256).max);

DutchOrder memory order = DutchOrder({
info: OrderInfoBuilder.init(address(reactor)).withSwapper(swapper).withDeadline(block.timestamp + 100)
.withValidationContract(priceOracleValidation).withValidationData(
// mock price oracle returns == outputAmount
abi.encode(address(mockPriceOracle), abi.encode(outputAmount))
),
decayStartTime: block.timestamp,
decayEndTime: block.timestamp + 100,
input: DutchInput(tokenIn, inputAmount, inputAmount),
outputs: OutputsBuilder.singleDutch(address(tokenOut), outputAmount, outputAmount, swapper)
});

// Below snapshot can be compared to `DutchExecuteSingle.snap` to compare an execute with and without
// exclusive filler validation
vm.startSnapshotGas("testPriceOracleValidationSucceeds");
fillContract.execute(SignedOrder(abi.encode(order), signOrder(swapperPrivateKey, address(permit2), order)));
vm.stopSnapshotGas();
assertEq(tokenOut.balanceOf(swapper), outputAmount);
assertEq(tokenIn.balanceOf(address(fillContract)), inputAmount);
}

// The filler is incorrectly address(0x123)
function testPriceOracleValidationFails() public {
uint256 inputAmount = 10 ** 18;
uint256 outputAmount = 2 * inputAmount;

tokenIn.mint(address(swapper), inputAmount);
tokenOut.mint(address(fillContract), outputAmount);
tokenIn.forceApprove(swapper, address(permit2), type(uint256).max);

DutchOrder memory order = DutchOrder({
info: OrderInfoBuilder.init(address(reactor)).withSwapper(swapper).withDeadline(block.timestamp + 100)
.withValidationContract(priceOracleValidation).withValidationData(
// mock price oracle returns < outputAmount
abi.encode(address(mockPriceOracle), abi.encode(outputAmount - 1))
),
decayStartTime: block.timestamp,
decayEndTime: block.timestamp + 100,
input: DutchInput(tokenIn, inputAmount, inputAmount),
outputs: OutputsBuilder.singleDutch(address(tokenOut), outputAmount, outputAmount, swapper)
});

vm.expectRevert(
abi.encodeWithSelector(PriceOracleValidation.InsufficientOutput.selector, outputAmount - 1, outputAmount)
);
fillContract.execute(SignedOrder(abi.encode(order), signOrder(swapperPrivateKey, address(permit2), order)));
}
}
Loading