Skip to content

Commit 62f076a

Browse files
authored
test: Orderbook TXE tests (#16451)
Fixes #14525 We didn't have the Orderbook thoroughly tested as when I implemented it TXE was not yet reasonable. Now it's reasonable so I decided to tackle this. Since AI is known to be good at tests I decided to gave it a try. ## AI experiment This was the prompt + well curated the context window by opening the relevant tabs in Cursor: > write the orderbook tests in noir-projects/noir-contracts/contracts/app/orderbook_contract/src/test.nr > > note that the Orderbook contract accepts 2 token contracts on the input so you will need to deploy those first. Use the setup and setup and mint functionality from noir-projects/noir-contracts/contracts/app/token_contract/src/test/utils.nr. > > From the orderbook you want to test the setup order and create order functions. There is also already a yarn-project/end-to-end/src/e2e_orderbook.test.ts test but that is written in TypeScript and noit Noir and it tests only the happy path. In the Noir tests I would also like to test unhappy paths. See tests in noir-projects/noir-contracts/contracts/app/token_contract/src/test/transfer_in_private.nr for inspiration on how to structure the Noir tests. Overall it managed to implement it all. The only thing I need to help it with is that it didn't understand that it needs to refer the `token_contract` crate when deploying: ```diff env.deploy("Token") env.deploy("@token_contract/Token") ``` Would say this was caused by us not having enough examples and me not putting that into a context window. Other than this it managed to resolve on its own one other issue (tried to mint with an account with token minter permissions). The only other thing I did is that I nuked quite a few tests it did because it was for example testing that a function call fails when it doesn't have token transfer authwit provided. Felt like this is sufficient to have only in the token tests. ### Update on AI Code quality was not great and had to redo quite a lot of stuff. That could be improved though if we had a proper reference (plenty of the style issues were present in the rest of our codebase).
2 parents d06ed9b + 9439f5d commit 62f076a

File tree

6 files changed

+242
-7
lines changed

6 files changed

+242
-7
lines changed

noir-projects/noir-contracts/contracts/app/amm_contract/src/test/test.nr

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ global AUTHWIT_NONCE: Field = 1;
99
global DEFAULT_AMOUNT_0_MIN: u128 = 0;
1010
global DEFAULT_AMOUNT_1_MIN: u128 = 0;
1111

12-
// Note: Ideally this test would be split into 2 separate test cases - one for order creation and one for order
13-
// fulfillment, with the second test running on the state from the first test. Since this feature is not currently
14-
// supported, combining them into a single comprehensive test is the best approach.
1512
#[test]
1613
unconstrained fn add_liquidity_twice_and_remove_liquidity() {
1714
let (mut env, amm_address, token0_address, token1_address, liquidity_token_address, minter) =

noir-projects/noir-contracts/contracts/app/orderbook_contract/src/main.nr

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod config;
22
mod order;
3+
mod test;
34

45
use aztec::macros::aztec;
56

@@ -97,7 +98,7 @@ pub contract Orderbook {
9798
let maker_partial_note =
9899
Token::at(ask_token).prepare_private_balance_increase(maker).call(&mut context);
99100

100-
// We use the partial note's as the order ID. Because partial notes emit a nullifier when created they are
101+
// We use the partial note as the order ID. Because partial notes emit a nullifier when created they are
101102
// unique, and so this guarantees that our order IDs are also unique without having to keep track of past
102103
// ones.
103104
let order_id = maker_partial_note.to_field();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mod test;
2+
pub(crate) mod utils;
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
use crate::{Orderbook, test::utils::setup};
2+
use aztec::{
3+
protocol_types::{address::AztecAddress, traits::FromField},
4+
test::helpers::authwit::add_private_authwit_from_call_interface,
5+
};
6+
use token::Token;
7+
use uint_note::uint_note::PartialUintNote;
8+
9+
global BID_AMOUNT: u128 = 1000;
10+
global ASK_AMOUNT: u128 = 2000;
11+
global CREATE_ORDER_AUTHWIT_NONCE: Field = 1;
12+
global FULFILL_ORDER_AUTHWIT_NONCE: Field = 2;
13+
14+
/// Checks the full flow of creating an order and fulfilling it.
15+
#[test]
16+
unconstrained fn full_flow() {
17+
let (mut env, orderbook_address, token0_address, token1_address, minter) = setup();
18+
19+
// We create a contract account (as opposed to a light account) for the maker and taker to be able to use authwits.
20+
let maker = env.create_contract_account();
21+
let taker = env.create_contract_account();
22+
23+
let token0 = Token::at(token0_address);
24+
let token1 = Token::at(token1_address);
25+
let orderbook = Orderbook::at(orderbook_address);
26+
27+
// Mint tokens to maker and taker
28+
env.call_private(minter, token0.mint_to_private(maker, BID_AMOUNT));
29+
env.call_private(minter, token1.mint_to_private(taker, ASK_AMOUNT));
30+
31+
// ORDER CREATION
32+
let transfer_call_interface =
33+
token0.transfer_to_public(maker, orderbook_address, BID_AMOUNT, CREATE_ORDER_AUTHWIT_NONCE);
34+
add_private_authwit_from_call_interface(maker, orderbook_address, transfer_call_interface);
35+
36+
let order_id = env.call_private(
37+
maker,
38+
orderbook.create_order(
39+
token0_address,
40+
token1_address,
41+
BID_AMOUNT,
42+
ASK_AMOUNT,
43+
CREATE_ORDER_AUTHWIT_NONCE,
44+
),
45+
);
46+
47+
// Get order and verify it's active
48+
let (order, is_fulfilled) = env.simulate_utility(orderbook.get_order(order_id));
49+
50+
assert_eq(order.bid_amount, BID_AMOUNT);
51+
assert_eq(order.ask_amount, ASK_AMOUNT);
52+
assert_eq(order.bid_token_is_zero, true); // token0 -> token1
53+
assert_eq(is_fulfilled, false);
54+
55+
// Verify that all maker's tokens were transferred to orderbook's public balance
56+
assert_eq(env.view_public(token0.balance_of_public(orderbook_address)), BID_AMOUNT);
57+
assert_eq(env.simulate_utility(token0.balance_of_private(maker)), 0);
58+
59+
// ORDER FULFILLMENT
60+
61+
// Convert order_id back to PartialUintNote as the orderbook does and then create authwit for taker to transfer ask
62+
// tokens.
63+
let maker_partial_note = PartialUintNote::from_field(order_id);
64+
let fulfill_transfer_call_interface = token1.finalize_transfer_to_private_from_private(
65+
taker,
66+
maker_partial_note,
67+
ASK_AMOUNT,
68+
FULFILL_ORDER_AUTHWIT_NONCE,
69+
);
70+
add_private_authwit_from_call_interface(
71+
taker,
72+
orderbook_address,
73+
fulfill_transfer_call_interface,
74+
);
75+
76+
env.call_private(taker, orderbook.fulfill_order(order_id, FULFILL_ORDER_AUTHWIT_NONCE));
77+
78+
// Verify final balances
79+
assert_eq(env.simulate_utility(token0.balance_of_private(maker)), 0);
80+
assert_eq(env.simulate_utility(token1.balance_of_private(maker)), ASK_AMOUNT);
81+
assert_eq(env.simulate_utility(token0.balance_of_private(taker)), BID_AMOUNT);
82+
assert_eq(env.simulate_utility(token1.balance_of_private(taker)), 0);
83+
84+
// Get order and verify it's fulfilled
85+
let (order, is_fulfilled) = env.simulate_utility(orderbook.get_order(order_id));
86+
87+
assert_eq(order.bid_amount, BID_AMOUNT);
88+
assert_eq(order.ask_amount, ASK_AMOUNT);
89+
assert_eq(order.bid_token_is_zero, true); // token0 -> token1
90+
assert_eq(is_fulfilled, true);
91+
}
92+
93+
#[test(should_fail_with = "ZERO_BID_AMOUNT")]
94+
unconstrained fn create_order_zero_bid_amount() {
95+
let (mut env, orderbook_address, token0_address, token1_address, _minter) = setup();
96+
97+
let orderbook = Orderbook::at(orderbook_address);
98+
let maker = env.create_light_account();
99+
100+
let _ = env.call_private(
101+
maker,
102+
orderbook.create_order(
103+
token0_address,
104+
token1_address,
105+
0,
106+
ASK_AMOUNT,
107+
CREATE_ORDER_AUTHWIT_NONCE,
108+
),
109+
);
110+
}
111+
112+
#[test(should_fail_with = "ZERO_ASK_AMOUNT")]
113+
unconstrained fn create_order_zero_ask_amount() {
114+
let (mut env, orderbook_address, token0_address, token1_address, _minter) = setup();
115+
116+
let maker = env.create_light_account();
117+
let orderbook = Orderbook::at(orderbook_address);
118+
119+
let _ = env.call_private(
120+
maker,
121+
orderbook.create_order(
122+
token0_address,
123+
token1_address,
124+
BID_AMOUNT,
125+
0,
126+
CREATE_ORDER_AUTHWIT_NONCE,
127+
),
128+
);
129+
}
130+
131+
#[test(should_fail_with = "BID_TOKEN_IS_INVALID")]
132+
unconstrained fn create_order_invalid_bid_token() {
133+
let (mut env, orderbook_address, _token0_address, token1_address, _minter) = setup();
134+
135+
let maker = env.create_light_account();
136+
let orderbook = Orderbook::at(orderbook_address);
137+
138+
let invalid_token = AztecAddress::from_field(999);
139+
140+
let _ = env.call_private(
141+
maker,
142+
orderbook.create_order(
143+
invalid_token,
144+
token1_address,
145+
BID_AMOUNT,
146+
ASK_AMOUNT,
147+
CREATE_ORDER_AUTHWIT_NONCE,
148+
),
149+
);
150+
}
151+
152+
#[test(should_fail_with = "ASK_TOKEN_IS_INVALID")]
153+
unconstrained fn create_order_invalid_ask_token() {
154+
let (mut env, orderbook_address, token0_address, _token1_address, _minter) = setup();
155+
156+
let maker = env.create_light_account();
157+
let orderbook = Orderbook::at(orderbook_address);
158+
159+
let invalid_token = AztecAddress::from_field(999);
160+
161+
let _ = env.call_private(
162+
maker,
163+
orderbook.create_order(
164+
token0_address,
165+
invalid_token,
166+
BID_AMOUNT,
167+
ASK_AMOUNT,
168+
CREATE_ORDER_AUTHWIT_NONCE,
169+
),
170+
);
171+
}
172+
173+
#[test(should_fail_with = "SAME_TOKEN_TRADE")]
174+
unconstrained fn create_order_same_tokens() {
175+
let (mut env, orderbook_address, token0_address, _token1_address, _minter) = setup();
176+
177+
let maker = env.create_light_account();
178+
let orderbook = Orderbook::at(orderbook_address);
179+
180+
let _ = env.call_private(
181+
maker,
182+
orderbook.create_order(
183+
token0_address,
184+
token0_address,
185+
BID_AMOUNT,
186+
ASK_AMOUNT,
187+
CREATE_ORDER_AUTHWIT_NONCE,
188+
),
189+
);
190+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use crate::Orderbook;
2+
use aztec::{
3+
protocol_types::address::AztecAddress, test::helpers::test_environment::TestEnvironment,
4+
};
5+
use token::Token;
6+
7+
// Sets up test env with orderbook with two tokens and a minter account.
8+
// TODO(#16560): Make it possible to return a contract instance directly from setup func
9+
pub(crate) unconstrained fn setup() -> (TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress) {
10+
let mut env = TestEnvironment::new();
11+
12+
// Setup admin account
13+
let admin = env.create_light_account();
14+
15+
// Deploy tokens
16+
let token0_initializer = Token::interface().constructor(
17+
admin,
18+
"Token00000000000000000000000000",
19+
"TK00000000000000000000000000000",
20+
18,
21+
);
22+
let token0_address =
23+
env.deploy("@token_contract/Token").with_public_initializer(admin, token0_initializer);
24+
25+
let token1_initializer = Token::interface().constructor(
26+
admin,
27+
"Token11111111111111111111111111",
28+
"TK11111111111111111111111111111",
29+
18,
30+
);
31+
let token1_address =
32+
env.deploy("@token_contract/Token").with_public_initializer(admin, token1_initializer);
33+
34+
// Deploy orderbook contract
35+
let orderbook_initializer = Orderbook::interface().constructor(token0_address, token1_address);
36+
let orderbook_address =
37+
env.deploy("Orderbook").with_public_initializer(admin, orderbook_initializer);
38+
39+
// The admin has the minter role. Since we only care about the minting capability in the tests, we return the admin
40+
// address as 'minter' rather than 'admin'.
41+
let minter = admin;
42+
43+
(env, orderbook_address, token0_address, token1_address, minter)
44+
}

yarn-project/end-to-end/src/e2e_orderbook.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import { setup } from './fixtures/utils.js';
99

1010
const TIMEOUT = 120_000;
1111

12-
// TODO(#14525): Write thorough Orderbook tests. Currently we test only a happy path here because we will migrate these
13-
// tests to TXE once TXE 2.0 is ready. Didn't write it in TXE now as there is no way to obtain public events and all of
14-
// TXE will be rewritten soon.
12+
// Unhappy path tests are written only in Noir.
13+
//
14+
// We keep this test around because it's the only TS test where we have async completion of a partial note (partial
15+
// note created in one tx and completed in another).
1516
describe('Orderbook', () => {
1617
jest.setTimeout(TIMEOUT);
1718

0 commit comments

Comments
 (0)