Skip to content

Commit 03427a7

Browse files
fhennekeharisang
andauthored
Add test for CoW AMM commitment (#109)
With enabling phase 2 of CoW AMM, solvers can execute CoW AMM orders with custom amounts. This requires commiting and uncommiting the order in the CoW AMM smart contract. Not uncommiting the order is a violation of social consensus rules since it prevents the default order from getting executed. This PR implements a test where after every settlement which contains a precommit interaction from the CoW AMM address the commited order for that address is checked. Since there seem to be no commits so far, the code is a bit difficult to test. I added a test with tailored call data. --------- Co-authored-by: harisang <harisangelidakis@gmail.com>
1 parent 481870e commit 03427a7

File tree

4 files changed

+349
-0
lines changed

4 files changed

+349
-0
lines changed

contracts/cowamm_constantproduct.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
cowamm_constantproduct = [
2+
{
3+
"inputs": [
4+
{"internalType": "address", "name": "_solutionSettler", "type": "address"}
5+
],
6+
"stateMutability": "nonpayable",
7+
"type": "constructor",
8+
},
9+
{"inputs": [], "name": "CommitOutsideOfSettlement", "type": "error"},
10+
{"inputs": [], "name": "OrderDoesNotMatchCommitmentHash", "type": "error"},
11+
{"inputs": [], "name": "OrderDoesNotMatchDefaultTradeableOrder", "type": "error"},
12+
{
13+
"inputs": [{"internalType": "string", "name": "", "type": "string"}],
14+
"name": "OrderNotValid",
15+
"type": "error",
16+
},
17+
{
18+
"inputs": [
19+
{"internalType": "uint256", "name": "blockNumber", "type": "uint256"},
20+
{"internalType": "string", "name": "message", "type": "string"},
21+
],
22+
"name": "PollTryAtBlock",
23+
"type": "error",
24+
},
25+
{
26+
"anonymous": False,
27+
"inputs": [
28+
{
29+
"indexed": True,
30+
"internalType": "address",
31+
"name": "owner",
32+
"type": "address",
33+
},
34+
{
35+
"components": [
36+
{
37+
"internalType": "contract IConditionalOrder",
38+
"name": "handler",
39+
"type": "address",
40+
},
41+
{"internalType": "bytes32", "name": "salt", "type": "bytes32"},
42+
{"internalType": "bytes", "name": "staticInput", "type": "bytes"},
43+
],
44+
"indexed": False,
45+
"internalType": "struct IConditionalOrder.ConditionalOrderParams",
46+
"name": "params",
47+
"type": "tuple",
48+
},
49+
],
50+
"name": "ConditionalOrderCreated",
51+
"type": "event",
52+
},
53+
{
54+
"inputs": [],
55+
"name": "EMPTY_COMMITMENT",
56+
"outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}],
57+
"stateMutability": "view",
58+
"type": "function",
59+
},
60+
{
61+
"inputs": [],
62+
"name": "MAX_ORDER_DURATION",
63+
"outputs": [{"internalType": "uint32", "name": "", "type": "uint32"}],
64+
"stateMutability": "view",
65+
"type": "function",
66+
},
67+
{
68+
"inputs": [
69+
{"internalType": "address", "name": "owner", "type": "address"},
70+
{"internalType": "bytes32", "name": "orderHash", "type": "bytes32"},
71+
],
72+
"name": "commit",
73+
"outputs": [],
74+
"stateMutability": "nonpayable",
75+
"type": "function",
76+
},
77+
{
78+
"inputs": [{"internalType": "address", "name": "", "type": "address"}],
79+
"name": "commitment",
80+
"outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}],
81+
"stateMutability": "view",
82+
"type": "function",
83+
},
84+
{
85+
"inputs": [
86+
{"internalType": "address", "name": "owner", "type": "address"},
87+
{"internalType": "address", "name": "", "type": "address"},
88+
{"internalType": "bytes32", "name": "", "type": "bytes32"},
89+
{"internalType": "bytes", "name": "staticInput", "type": "bytes"},
90+
{"internalType": "bytes", "name": "", "type": "bytes"},
91+
],
92+
"name": "getTradeableOrder",
93+
"outputs": [
94+
{
95+
"components": [
96+
{
97+
"internalType": "contract IERC20",
98+
"name": "sellToken",
99+
"type": "address",
100+
},
101+
{
102+
"internalType": "contract IERC20",
103+
"name": "buyToken",
104+
"type": "address",
105+
},
106+
{"internalType": "address", "name": "receiver", "type": "address"},
107+
{
108+
"internalType": "uint256",
109+
"name": "sellAmount",
110+
"type": "uint256",
111+
},
112+
{"internalType": "uint256", "name": "buyAmount", "type": "uint256"},
113+
{"internalType": "uint32", "name": "validTo", "type": "uint32"},
114+
{"internalType": "bytes32", "name": "appData", "type": "bytes32"},
115+
{"internalType": "uint256", "name": "feeAmount", "type": "uint256"},
116+
{"internalType": "bytes32", "name": "kind", "type": "bytes32"},
117+
{
118+
"internalType": "bool",
119+
"name": "partiallyFillable",
120+
"type": "bool",
121+
},
122+
{
123+
"internalType": "bytes32",
124+
"name": "sellTokenBalance",
125+
"type": "bytes32",
126+
},
127+
{
128+
"internalType": "bytes32",
129+
"name": "buyTokenBalance",
130+
"type": "bytes32",
131+
},
132+
],
133+
"internalType": "struct GPv2Order.Data",
134+
"name": "",
135+
"type": "tuple",
136+
}
137+
],
138+
"stateMutability": "view",
139+
"type": "function",
140+
},
141+
{
142+
"inputs": [],
143+
"name": "solutionSettler",
144+
"outputs": [{"internalType": "address", "name": "", "type": "address"}],
145+
"stateMutability": "view",
146+
"type": "function",
147+
},
148+
{
149+
"inputs": [{"internalType": "bytes4", "name": "interfaceId", "type": "bytes4"}],
150+
"name": "supportsInterface",
151+
"outputs": [{"internalType": "bool", "name": "", "type": "bool"}],
152+
"stateMutability": "view",
153+
"type": "function",
154+
},
155+
{
156+
"inputs": [
157+
{"internalType": "address", "name": "owner", "type": "address"},
158+
{"internalType": "address", "name": "", "type": "address"},
159+
{"internalType": "bytes32", "name": "orderHash", "type": "bytes32"},
160+
{"internalType": "bytes32", "name": "", "type": "bytes32"},
161+
{"internalType": "bytes32", "name": "", "type": "bytes32"},
162+
{"internalType": "bytes", "name": "staticInput", "type": "bytes"},
163+
{"internalType": "bytes", "name": "", "type": "bytes"},
164+
{
165+
"components": [
166+
{
167+
"internalType": "contract IERC20",
168+
"name": "sellToken",
169+
"type": "address",
170+
},
171+
{
172+
"internalType": "contract IERC20",
173+
"name": "buyToken",
174+
"type": "address",
175+
},
176+
{"internalType": "address", "name": "receiver", "type": "address"},
177+
{
178+
"internalType": "uint256",
179+
"name": "sellAmount",
180+
"type": "uint256",
181+
},
182+
{"internalType": "uint256", "name": "buyAmount", "type": "uint256"},
183+
{"internalType": "uint32", "name": "validTo", "type": "uint32"},
184+
{"internalType": "bytes32", "name": "appData", "type": "bytes32"},
185+
{"internalType": "uint256", "name": "feeAmount", "type": "uint256"},
186+
{"internalType": "bytes32", "name": "kind", "type": "bytes32"},
187+
{
188+
"internalType": "bool",
189+
"name": "partiallyFillable",
190+
"type": "bool",
191+
},
192+
{
193+
"internalType": "bytes32",
194+
"name": "sellTokenBalance",
195+
"type": "bytes32",
196+
},
197+
{
198+
"internalType": "bytes32",
199+
"name": "buyTokenBalance",
200+
"type": "bytes32",
201+
},
202+
],
203+
"internalType": "struct GPv2Order.Data",
204+
"name": "order",
205+
"type": "tuple",
206+
},
207+
],
208+
"name": "verify",
209+
"outputs": [],
210+
"stateMutability": "view",
211+
"type": "function",
212+
},
213+
]

src/daemon.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
from src.monitoring_tests.uniform_directed_prices_test import (
2828
UniformDirectedPricesTest,
2929
)
30+
from src.monitoring_tests.cowamm_commitment_test import (
31+
CoWAMMCommitmentTest,
32+
)
3033
from src.constants import SLEEP_TIME_IN_SEC
3134

3235

@@ -44,6 +47,7 @@ def main() -> None:
4447
BuffersMonitoringTest(),
4548
CombinatorialAuctionSurplusTest(),
4649
UniformDirectedPricesTest(),
50+
CoWAMMCommitmentTest(),
4751
]
4852

4953
start_block: Optional[int] = None
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
Checks that commitments for custom CoW AMM of CoW AMM orders are reset.
3+
"""
4+
# pylint: disable=duplicate-code
5+
from typing import Any
6+
7+
from eth_typing import Address
8+
from hexbytes import HexBytes
9+
10+
from src.monitoring_tests.base_test import BaseTest
11+
from src.apis.web3api import Web3API
12+
13+
from contracts.cowamm_constantproduct import cowamm_constantproduct
14+
15+
EMPTY_COMMITMENT = "0x0000000000000000000000000000000000000000000000000000000000000000"
16+
COWAMM_CONSTANT_PRODUCT_ADDRESS = "0x34323B933096534e43958F6c7Bf44F2Bb59424DA".lower()
17+
18+
19+
class CoWAMMCommitmentTest(BaseTest):
20+
"""Checks that commitments for custom CoW AMM of CoW AMM orders are reset
21+
22+
Whenever a preinteraction calling the commit functinon of the (old) CoW AMM smart contract is
23+
called, the currently commit order on the corresponding CoW AMM is checked. If the commited
24+
order is not equal to the default order, the commitment was not reset.
25+
"""
26+
27+
def __init__(self) -> None:
28+
super().__init__()
29+
self.web3_api = Web3API()
30+
self.contract = self.web3_api.web_3.eth.contract(
31+
address=Address(HexBytes(COWAMM_CONSTANT_PRODUCT_ADDRESS)),
32+
abi=cowamm_constantproduct,
33+
)
34+
35+
def check_commitments(self, settlement: dict[str, Any]) -> bool:
36+
"""Checks the commitment of CoW AMM orders.
37+
38+
If there is a preinteraction with a call to the commit function of the CoW AMM contant
39+
product smart contract, the commitment of that AMM is checked at the current point in
40+
time.
41+
42+
This is not a check for including a commit in post interactions or within interactions.
43+
It also does not check if the uncommit happened immediately, but just checks if the
44+
current commitment is to the default order.
45+
"""
46+
# iterate over pre-interactions
47+
for interaction in settlement["interactions"][0]:
48+
if interaction["target"] != COWAMM_CONSTANT_PRODUCT_ADDRESS:
49+
continue
50+
51+
cowamm_address = self.get_cowamm_address(interaction)
52+
53+
commitment = self.get_commitment(cowamm_address)
54+
if commitment is None:
55+
return False
56+
57+
commitment_is_reset = commitment == EMPTY_COMMITMENT
58+
59+
log_output = "\t".join(
60+
[
61+
"CoW AMM Commitment test",
62+
f"Commitment reset: {commitment_is_reset}",
63+
f"CoW AMM: {cowamm_address}",
64+
f"Commitment: {commitment}",
65+
]
66+
)
67+
if not commitment_is_reset:
68+
self.alert(log_output)
69+
else:
70+
self.logger.info(log_output)
71+
72+
return True
73+
74+
def get_cowamm_address(self, interaction: dict[str, Any]) -> str:
75+
"""Get the address of the CoW AMM from the commit interaction"""
76+
decoded_interaction = self.contract.decode_function_input(
77+
interaction["callData"]
78+
)[1]
79+
cowamm_address = str(decoded_interaction["owner"])
80+
return cowamm_address
81+
82+
def get_commitment(self, cowamm: str) -> str | None:
83+
"""Get the commited order for a given CoW AMM
84+
It calls the commitment function in the smart contract for a given address and returns a
85+
string of the format "0x...".
86+
"""
87+
return "0x" + str(self.contract.functions.commitment(cowamm).call().hex())
88+
89+
def run(self, tx_hash: str) -> bool:
90+
"""
91+
Wrapper function for the whole test. Checks that commitments for custom CoW AMM of CoW
92+
AMM orders are reset
93+
"""
94+
95+
transaction = self.web3_api.get_transaction(tx_hash)
96+
if transaction is None:
97+
return False
98+
settlement = self.web3_api.get_settlement(transaction)
99+
100+
success = self.check_commitments(settlement)
101+
102+
return success

tests/e2e/cowamm_commitment_test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
Tests for CoW AMM commitment test.
3+
"""
4+
5+
import unittest
6+
from src.monitoring_tests.cowamm_commitment_test import (
7+
CoWAMMCommitmentTest,
8+
COWAMM_CONSTANT_PRODUCT_ADDRESS,
9+
)
10+
11+
12+
class TestCoWAMMCommitment(unittest.TestCase):
13+
def test_cowamm_commitment(self) -> None:
14+
surplus_test = CoWAMMCommitmentTest()
15+
# using dummy call data which encodes one active CoW AMM
16+
settlement = {
17+
"interactions": [
18+
[
19+
{
20+
"target": COWAMM_CONSTANT_PRODUCT_ADDRESS,
21+
"callData": "0x30f73c99000000000000000000000000beef5afe88ef73337e5070ab2855d37dbf5493a40000000000000000000000000000000000000000000000000000000000000001",
22+
}
23+
]
24+
]
25+
}
26+
self.assertTrue(surplus_test.check_commitments(settlement))
27+
28+
29+
if __name__ == "__main__":
30+
unittest.main()

0 commit comments

Comments
 (0)