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

Add order commitment check on verification #21

Merged
merged 5 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 18 additions & 0 deletions script/libraries/Utils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import {Script} from "forge-std/Script.sol";

abstract contract Utils is Script {
function addressEnvOrDefault(string memory envName, address defaultAddr) internal view returns (address) {
try vm.envAddress(envName) returns (address env) {
return env;
} catch {
return defaultAddr;
}
}

function assertHasCode(address a, string memory context) internal view {
require(a.code.length > 0, context);
}
}
29 changes: 12 additions & 17 deletions script/single-deployment/BalancerWeightedPoolPriceOracle.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,27 @@ pragma solidity >=0.8.0 <0.9.0;

import {Script, console} from "forge-std/Script.sol";

import {Utils} from "../libraries/Utils.sol";

import {BalancerWeightedPoolPriceOracle, IVault} from "src/oracles/BalancerWeightedPoolPriceOracle.sol";

contract DeployBalancerWeightedPoolPriceOracle is Script {
string constant balancerVaultEnv = "BALANCER_VAULT";
contract DeployBalancerWeightedPoolPriceOracle is Script, Utils {
// Balancer uses the same address on each supported chain until now:
// https://docs.balancer.fi/reference/contracts/deployment-addresses/mainnet.html
// Chains: Arbitrum, Avalanche, Base, Gnosis, Goerli, Mainnet, Optimism,
// Polygon, Sepolia, Zkevm
address internal constant DEFAULT_BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
IVault internal vault;

constructor() {
try vm.envAddress(balancerVaultEnv) returns (address vault_) {
vault = IVault(vault_);
} catch {
vault = defaultBalancerVault();
}
console.log("Balancer vault at %s.", address(vault));
address vaultAddress = addressEnvOrDefault("BALANCER_VAULT", DEFAULT_BALANCER_VAULT);
console.log("Balancer vault at %s.", vaultAddress);
// We assume that if there's code at that address, then it's a Balancer
// vault deployment. This isn't guaranteed because they don't use
// deterministic addresses and in theory there could be any contract
// there.
require(address(vault).code.length > 0, "no code at expected Balancer vault");
assertHasCode(vaultAddress, "no code at expected Balancer vault");
vault = IVault(vaultAddress);
}

function run() public virtual {
Expand All @@ -31,12 +34,4 @@ contract DeployBalancerWeightedPoolPriceOracle is Script {
vm.broadcast();
return new BalancerWeightedPoolPriceOracle(vault);
}

function defaultBalancerVault() internal pure returns (IVault) {
// Balancer uses the same address on each supported chain until now:
// https://docs.balancer.fi/reference/contracts/deployment-addresses/mainnet.html
// Chains: Arbitrum, Avalanche, Base, Gnosis, Goerli, Mainnet, Optimism,
// Polygon, Sepolia, Zkevm
return IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
}
}
17 changes: 14 additions & 3 deletions script/single-deployment/ConstantProduct.s.sol
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import {Script} from "forge-std/Script.sol";
import {Script, console} from "forge-std/Script.sol";

import {Utils} from "../libraries/Utils.sol";

import {ConstantProduct} from "src/ConstantProduct.sol";

contract DeployConstantProduct is Script {
contract DeployConstantProduct is Script, Utils {
address internal constant DEFAULT_SETTLEMENT_CONTRACT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41;
address internal solutionSettler;

constructor() {
solutionSettler = addressEnvOrDefault("SETTLEMENT_CONTRACT", DEFAULT_SETTLEMENT_CONTRACT);
console.log("Settlement contract at %s.", solutionSettler);
assertHasCode(solutionSettler, "no code at expected settlement contract");
}

function run() public virtual {
deployConstantProduct();
}

function deployConstantProduct() internal returns (ConstantProduct) {
vm.broadcast();
return new ConstantProduct();
return new ConstantProduct(solutionSettler);
}
}
137 changes: 128 additions & 9 deletions src/ConstantProduct.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import {IWatchtowerCustomErrors} from "./interfaces/IWatchtowerCustomErrors.sol"
* Order creation and execution is based on the Composable CoW base contracts.
*/
contract ConstantProduct is IConditionalOrderGenerator {
uint32 public constant MAX_ORDER_DURATION = 5 * 60;

/// All data used by an order to validate the AMM conditions.
struct Data {
/// The first of the tokens traded by this AMM.
Expand All @@ -45,6 +43,61 @@ contract ConstantProduct is IConditionalOrderGenerator {
bytes32 appData;
}

/**
* @notice The largest possible duration of any AMM order, starting from the
* current block timestamp.
*/
uint32 public constant MAX_ORDER_DURATION = 5 * 60;
/**
* @notice The value representing the absence of a commitment. It signifies
* that the AMM will enforce that the order matches the order obtained from
* calling `getTradeableOrder`.
*/
bytes32 public constant EMPTY_COMMITMENT = bytes32(0);
/**
* @notice The address of the CoW Protocol settlement contract. It is the
* only address that can set commitments.
*/
address public immutable solutionSettler;
/**
* @notice It associates every order owner to the only order hash that can
* be validated by calling `verify`. The hash corresponding to the constant
* `EMPTY_COMMITMENT` has special semantics, discussed in the related
* documentation.
*/
mapping(address => bytes32) public commitment;

/**
* @notice The `commit` function can only be called inside a CoW Swap
* settlement. This error is thrown when the function is called from another
* context.
*/
error CommitOutsideOfSettlement();

/**
* @param _solutionSettler The CoW Protocol contract used to settle user
* orders on the current chain.
*/
constructor(address _solutionSettler) {
solutionSettler = _solutionSettler;
}

/**
* @notice Restricts a specific AMM to being able to trade only the order
* with the specified hash.
* @dev The commitment is used to enforce that exactly one AMM order is
* valid when a CoW Protocol batch is settled.
* @param owner the commitment applies to orders created by this address.
* @param orderHash the order hash that will be enforced by the order
* verification function.
*/
function commit(address owner, bytes32 orderHash) public {
if (msg.sender != solutionSettler) {
revert CommitOutsideOfSettlement();
}
commitment[owner] = orderHash;
}

/**
* @notice The order returned by this function is the order that needs to be
* executed for the price on the owner AMM to match that of the reference
Expand All @@ -65,8 +118,6 @@ contract ConstantProduct is IConditionalOrderGenerator {
* parameters that are required for order creation. Compared to implementing
* the logic inside the original function, it frees up some stack slots and
* reduces "stack too deep" issues.
* @dev We are not interested in the gas efficiency of this function because
* it is not supposed to be called by a call in the blockchain.
* @param owner the contract who is the owner of the order
* @param staticInput the static input for all discrete orders cut from this
* conditional order
Expand Down Expand Up @@ -156,14 +207,14 @@ contract ConstantProduct is IConditionalOrderGenerator {
function verify(
address owner,
address,
bytes32,
bytes32 orderHash,
bytes32,
bytes32,
bytes calldata staticInput,
bytes calldata,
GPv2Order.Data calldata order
) external view override {
_verify(owner, staticInput, order);
_verify(owner, orderHash, staticInput, order);
}

/**
Expand All @@ -172,11 +223,17 @@ contract ConstantProduct is IConditionalOrderGenerator {
* `verify`, it frees up some stack slots and reduces "stack too deep"
* issues.
* @param owner the contract who is the owner of the order
* @param orderHash the hash of the current order as defined by the
* `GPv2Order` library.
* @param staticInput the static input for all discrete orders cut from this
* conditional order
* @param order `GPv2Order.Data` of a discrete order to be verified.
*/
function _verify(address owner, bytes calldata staticInput, GPv2Order.Data calldata order) internal view {
function _verify(address owner, bytes32 orderHash, bytes calldata staticInput, GPv2Order.Data calldata order)
internal
view
{
requireMatchingCommitment(owner, orderHash, staticInput, order);
ConstantProduct.Data memory data = abi.decode(staticInput, (Data));

IERC20 sellToken = data.token0;
Expand Down Expand Up @@ -221,8 +278,8 @@ contract ConstantProduct is IConditionalOrderGenerator {
}

// No checks on:
//bytes32 kind;
//bool partiallyFillable;
// - kind
// - partiallyFillable
}

/**
Expand Down Expand Up @@ -259,4 +316,66 @@ contract ConstantProduct is IConditionalOrderGenerator {
function revertPollAtNextBucket(string memory message) internal view {
revert IWatchtowerCustomErrors.PollTryAtBlock(block.number + 1, message);
}

/**
* @notice This function triggers a revert if either (1) the order hash does
* not match the current commitment of the owner, or (2) in the case of a
* commitment to `EMPTY_COMMITMENT`, the non-constant parameters of the
* order from `getTradeableOrder` don't match those of the input order.
* @param owner the contract that owns the order
* @param orderHash the hash of the current order as defined by the
* `GPv2Order` library.
* @param staticInput the static input for all discrete orders cut from this
* conditional order
* @param order `GPv2Order.Data` of a discrete order to be verified
*/
function requireMatchingCommitment(
address owner,
bytes32 orderHash,
bytes calldata staticInput,
GPv2Order.Data calldata order
) internal view {
bytes32 committedOrderHash = commitment[owner];
if (orderHash != committedOrderHash) {
if (committedOrderHash != EMPTY_COMMITMENT) {
revert IConditionalOrder.OrderNotValid("commitment not matching");
fedgiac marked this conversation as resolved.
Show resolved Hide resolved
}
GPv2Order.Data memory computedOrder = _getTradeableOrder(owner, staticInput);
if (!matchFreeOrderParams(order, computedOrder)) {
revert IConditionalOrder.OrderNotValid("getTradeableOrder not matching");
}
}
}

/**
* @notice Check if the parameters of the two input orders are the same,
* with the exception of those parameters that have a single possible value
* that passes the validation of `verify`.
* @param lhs a CoW Swap order
* @param rhs another CoW Swap order
* @return true if the order parameters match, false otherwise
*/
function matchFreeOrderParams(GPv2Order.Data calldata lhs, GPv2Order.Data memory rhs)
internal
pure
returns (bool)
{
bool sameSellToken = lhs.sellToken == rhs.sellToken;
bool sameBuyToken = lhs.buyToken == rhs.buyToken;
bool sameSellAmount = lhs.sellAmount == rhs.sellAmount;
bool sameBuyAmount = lhs.buyAmount == rhs.buyAmount;
bool sameValidTo = lhs.validTo == rhs.validTo;
bool sameKind = lhs.kind == rhs.kind;
bool samePartiallyFillable = lhs.partiallyFillable == rhs.partiallyFillable;

// The following parameters are untested:
// - receiver
// - appData
// - feeAmount
// - sellTokenBalance
// - buyTokenBalance

return sameSellToken && sameBuyToken && sameSellAmount && sameBuyAmount && sameValidTo && sameKind
&& samePartiallyFillable;
}
}
3 changes: 2 additions & 1 deletion test/ConstantProduct.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ pragma solidity ^0.8.13;

import {VerifyTest} from "./ConstantProduct/VerifyTest.sol";
import {GetTradeableOrderTest} from "./ConstantProduct/GetTradeableOrderTest.sol";
import {CommitTest} from "./ConstantProduct/CommitTest.sol";

contract ConstantProductTest is VerifyTest, GetTradeableOrderTest {}
contract ConstantProductTest is VerifyTest, GetTradeableOrderTest, CommitTest {}
33 changes: 33 additions & 0 deletions test/ConstantProduct/CommitTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import {Utils} from "test/libraries/Utils.sol";
import {ConstantProductTestHarness, ConstantProduct} from "./ConstantProductTestHarness.sol";

abstract contract CommitTest is ConstantProductTestHarness {
function testSolutionSettlerCanSetAnyCommit() public {
vm.prank(solutionSettler);
constantProduct.commit(
0x1337133713371337133713371337133713371337,
0x4242424242424242424242424242424242424242424242424242424242424242
);
}

function testCommitIsPermissioned() public {
vm.prank(Utils.addressFromString("some random address"));
vm.expectRevert(abi.encodeWithSelector(ConstantProduct.CommitOutsideOfSettlement.selector));
constantProduct.commit(
0x1337133713371337133713371337133713371337,
0x4242424242424242424242424242424242424242424242424242424242424242
);
}

function testCommittingSetsCommitmentInMapping() public {
address addr = 0x1337133713371337133713371337133713371337;
bytes32 commitment = 0x4242424242424242424242424242424242424242424242424242424242424242;
assertEq(constantProduct.commitment(addr), bytes32(0));
vm.prank(solutionSettler);
constantProduct.commit(addr, commitment);
assertEq(constantProduct.commitment(addr), commitment);
}
}
26 changes: 21 additions & 5 deletions test/ConstantProduct/ConstantProductTestHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ abstract contract ConstantProductTestHarness is BaseComposableCoWTest {
address private DEFAULT_PAIR = Utils.addressFromString("default USDC/WETH pair");
address private DEFAULT_RECEIVER = Utils.addressFromString("default receiver");
bytes32 private DEFAULT_APPDATA = keccak256(bytes("unit test"));
bytes32 private DEFAULT_COMMITMENT = keccak256(bytes("order hash"));

ConstantProduct constantProduct;
UniswapV2PriceOracle uniswapV2PriceOracle;
address internal solutionSettler = Utils.addressFromString("settlment contract");
fedgiac marked this conversation as resolved.
Show resolved Hide resolved
ConstantProduct internal constantProduct;
UniswapV2PriceOracle internal uniswapV2PriceOracle;

function setUp() public virtual override(BaseComposableCoWTest) {
super.setUp();

constantProduct = new ConstantProduct();
constantProduct = new ConstantProduct(solutionSettler);
uniswapV2PriceOracle = new UniswapV2PriceOracle();
}

Expand Down Expand Up @@ -50,6 +52,11 @@ abstract contract ConstantProductTestHarness is BaseComposableCoWTest {
return getDefaultData();
}

function setUpDefaultCommitment(address owner) internal {
vm.prank(solutionSettler);
constantProduct.commit(owner, DEFAULT_COMMITMENT);
}

function setUpDefaultReserves(address owner) internal {
setUpDefaultWithReserves(owner, 1337, 1337);
}
Expand Down Expand Up @@ -109,15 +116,24 @@ abstract contract ConstantProductTestHarness is BaseComposableCoWTest {
}

// This function calls `verify` while filling all unused parameters with
// arbitrary data.
// arbitrary data and the order hash with the default commitment.
function verifyWrapper(address owner, ConstantProduct.Data memory staticInput, GPv2Order.Data memory order)
internal
view
{
verifyWrapper(owner, DEFAULT_COMMITMENT, staticInput, order);
}

function verifyWrapper(
address owner,
bytes32 orderHash,
ConstantProduct.Data memory staticInput,
GPv2Order.Data memory order
) internal view {
constantProduct.verify(
owner,
Utils.addressFromString("sender"),
keccak256(bytes("order hash")),
orderHash,
keccak256(bytes("domain separator")),
keccak256(bytes("context")),
abi.encode(staticInput),
Expand Down
Loading