Skip to content

Commit

Permalink
Merge pull request #5 from haiko-xyz/audit/nm-audit-fix
Browse files Browse the repository at this point in the history
audit: NM audit fixes +`ISolverHooks` refactor
  • Loading branch information
parketh authored Jul 11, 2024
2 parents 80fb8e7 + 2ec693b commit ae6ea21
Show file tree
Hide file tree
Showing 30 changed files with 1,280 additions and 570 deletions.
10 changes: 5 additions & 5 deletions docs/1-technical-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ fn deposit(
) -> (u256, u256, u256);
```

Withdrawals can be made either by calling `withdraw_at_ratio()` to withdraw from a public vault, or `withdraw()` to withdraw an arbitrary amounts from a private vault (available to the vault owner only).
Withdrawals can be made either by calling `withdraw_public()` to withdraw from a public vault, or `withdraw_private()` to withdraw an arbitrary amounts from a private vault (available to the vault owner only).

```rust
// Burn pool shares and withdraw funds from market.
// Called for public vaults. For private vaults, use `withdraw_amount`.
// Called for public vaults. For private vaults, use `withdraw_private`.
//
// # Arguments
// * `market_id` - market id
Expand All @@ -157,12 +157,12 @@ Withdrawals can be made either by calling `withdraw_at_ratio()` to withdraw from
// # Returns
// * `base_amount` - base asset withdrawn
// * `quote_amount` - quote asset withdrawn
fn withdraw_at_ratio(
fn withdraw_public(
ref self: TContractState, market_id: felt252, shares: u256
) -> (u256, u256);

// Withdraw exact token amounts from market.
// Called for private vaults. For public vaults, use `withdraw_at_ratio`.
// Called for private vaults. For public vaults, use `withdraw_public`.
//
// # Arguments
// * `market_id` - market id
Expand All @@ -172,7 +172,7 @@ fn withdraw_at_ratio(
// # Returns
// * `base_amount` - base asset withdrawn
// * `quote_amount` - quote asset withdrawn
fn withdraw(
fn withdraw_private(
ref self: TContractState, market_id: felt252, base_amount: u256, quote_amount: u256
) -> (u256, u256);
```
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/contracts/mocks/mock_solver.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,14 @@ pub mod MockSolver {
}

// Callback function to execute any state updates after a swap is completed.
// This fn should only be callable by the solver contract.
//
// # Arguments
// * `market_id` - market id
// * `swap_params` - swap parameters
fn after_swap(ref self: ContractState, market_id: felt252, swap_params: SwapParams) {}
fn after_swap(ref self: ContractState, market_id: felt252, swap_params: SwapParams) {
assert(self.solver.unlocked.read(), 'NotSolver');
}
}

#[abi(embed_v0)]
Expand Down
26 changes: 19 additions & 7 deletions packages/core/src/contracts/solver.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub mod SolverComponent {
withdraw_fees: LegacyMap::<ContractAddress, u256>,
// vault token class hash
vault_token_class: ClassHash,
// reentrancy guard (unlocked for hook calls)
unlocked: bool,
}

////////////////////////////////
Expand Down Expand Up @@ -377,6 +379,7 @@ pub mod SolverComponent {
// Check params.
assert(market_info.base_token != contract_address_const::<0x0>(), 'BaseTokenNull');
assert(market_info.quote_token != contract_address_const::<0x0>(), 'QuoteTokenNull');
assert(market_info.base_token != market_info.quote_token, 'SameToken');
assert(market_info.owner != contract_address_const::<0x0>(), 'OwnerNull');

// Set market info.
Expand All @@ -387,7 +390,7 @@ pub mod SolverComponent {
if market_info.is_public {
let vault_token_addr = self._deploy_vault_token(market_info);
vault_token = Option::Some(vault_token_addr);
let mut state: MarketState = self.market_state.read(market_id);
let mut state: MarketState = Default::default();
state.vault_token = vault_token_addr;
self.market_state.write(market_id, state);
}
Expand Down Expand Up @@ -426,6 +429,12 @@ pub mod SolverComponent {
fn swap(
ref self: ComponentState<TContractState>, market_id: felt252, swap_params: SwapParams,
) -> (u256, u256) {
// Run validity checks.
let state: MarketState = self.market_state.read(market_id);
let market_info: MarketInfo = self.market_info.read(market_id);
assert(market_info.base_token != contract_address_const::<0x0>(), 'MarketNull');
assert(!state.is_paused, 'Paused');

// Get amounts.
let solver_hooks = ISolverHooksDispatcher { contract_address: get_contract_address() };
let (amount_in, amount_out) = solver_hooks.quote(market_id, swap_params);
Expand Down Expand Up @@ -469,7 +478,9 @@ pub mod SolverComponent {
self.market_state.write(market_id, state);

// Execute after swap hook.
self.unlocked.write(true);
solver_hooks.after_swap(market_id, swap_params);
self.unlocked.write(false);

// Emit events.
self
Expand Down Expand Up @@ -754,7 +765,7 @@ pub mod SolverComponent {
}

// Burn pool shares and withdraw funds from market.
// Called for public vaults. For private vaults, use `withdraw_amount`.
// Called for public vaults. For private vaults, use `withdraw_private`.
//
// # Arguments
// * `market_id` - market id
Expand All @@ -763,7 +774,7 @@ pub mod SolverComponent {
// # Returns
// * `base_amount` - base asset withdrawn
// * `quote_amount` - quote asset withdrawn
fn withdraw_at_ratio(
fn withdraw_public(
ref self: ComponentState<TContractState>, market_id: felt252, shares: u256
) -> (u256, u256) {
// Fetch state.
Expand All @@ -773,7 +784,7 @@ pub mod SolverComponent {
// Run checks.
assert(market_info.base_token != contract_address_const::<0x0>(), 'MarketNull');
assert(shares != 0, 'SharesZero');
assert(market_info.is_public, 'UseWithdraw');
assert(market_info.is_public, 'UseWithdrawPrivate');
let vault_token = ERC20ABIDispatcher { contract_address: state.vault_token };
let caller = get_caller_address();
assert(shares <= vault_token.balanceOf(caller), 'InsuffShares');
Expand All @@ -795,7 +806,7 @@ pub mod SolverComponent {
}

// Withdraw exact token amounts from market.
// Called for private vaults. For public vaults, use `withdraw_at_ratio`.
// Called for private vaults. For public vaults, use `withdraw_public`.
//
// # Arguments
// * `market_id` - market id
Expand All @@ -805,7 +816,7 @@ pub mod SolverComponent {
// # Returns
// * `base_amount` - base asset withdrawn
// * `quote_amount` - quote asset withdrawn
fn withdraw(
fn withdraw_private(
ref self: ComponentState<TContractState>,
market_id: felt252,
base_amount: u256,
Expand All @@ -817,7 +828,8 @@ pub mod SolverComponent {

// Run checks.
assert(market_info.base_token != contract_address_const::<0x0>(), 'MarketNull');
assert(!market_info.is_public, 'UseWithdrawAtRatio');
assert(!market_info.is_public, 'UseWithdrawPublic');
self.assert_market_owner(market_id);

// Cap withdraw amount at available. Commit state changes.
let base_withdraw = min(base_amount, state.base_reserves);
Expand Down
12 changes: 5 additions & 7 deletions packages/core/src/interfaces/ISolver.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ pub trait ISolver<TContractState> {
) -> (u256, u256, u256);

// Burn pool shares and withdraw funds from market.
// Called for public vaults. For private vaults, use `withdraw_amount`.
// Called for public vaults. For private vaults, use `withdraw_private`.
//
// # Arguments
// * `market_id` - market id
Expand All @@ -187,12 +187,10 @@ pub trait ISolver<TContractState> {
// # Returns
// * `base_amount` - base asset withdrawn
// * `quote_amount` - quote asset withdrawn
fn withdraw_at_ratio(
ref self: TContractState, market_id: felt252, shares: u256
) -> (u256, u256);
fn withdraw_public(ref self: TContractState, market_id: felt252, shares: u256) -> (u256, u256);

// Withdraw exact token amounts from market.
// Called for private vaults. For public vaults, use `withdraw_at_ratio`.
// Called for private vaults. For public vaults, use `withdraw_public`.
//
// # Arguments
// * `market_id` - market id
Expand All @@ -202,7 +200,7 @@ pub trait ISolver<TContractState> {
// # Returns
// * `base_amount` - base asset withdrawn
// * `quote_amount` - quote asset withdrawn
fn withdraw(
fn withdraw_private(
ref self: TContractState, market_id: felt252, base_amount: u256, quote_amount: u256
) -> (u256, u256);

Expand Down Expand Up @@ -263,7 +261,7 @@ pub trait ISolver<TContractState> {
fn upgrade(ref self: TContractState, new_class_hash: ClassHash);
}

// Solvers must implement the `ISolverHooks` interface, which returns the quote for a swap.
// Solvers must implement the `ISolverHooks` interface.
#[starknet::interface]
pub trait ISolverHooks<TContractState> {
// Obtain quote for swap through a market.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ fn test_erc20_bytearray() {
symbol.serialize(ref calldata);
let contract_address = erc20_class.deploy(@calldata).unwrap();
let result = erc20_versioned_call::get_symbol(contract_address);
println!("result(bytearray): {}", result);
assert(result == "MOCK", 'Symbol (byte array)');
}

Expand All @@ -38,6 +37,5 @@ fn test_erc20_felt252() {
symbol.serialize(ref calldata);
let contract_address = erc20_class.deploy(@calldata).unwrap();
let result = erc20_versioned_call::get_symbol(contract_address);
println!("result(felt252): {}", result);
assert(result == "MOCK", 'Symbol (felt252)');
}
19 changes: 19 additions & 0 deletions packages/core/src/tests/solver/test_create_market.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,25 @@ fn test_create_market_with_null_quote_token_fails() {
solver.create_market(market_info);
}

#[test]
#[should_panic(expected: ('SameToken',))]
fn test_create_market_with_same_token_fails() {
let (base_token, _quote_token, _vault_token_class, solver, _market_id, _vault_token_opt) =
before(
true
);

// Create market.
start_prank(CheatTarget::One(solver.contract_address), owner());
let market_info = MarketInfo {
base_token: base_token.contract_address,
quote_token: base_token.contract_address,
owner: alice(),
is_public: true,
};
solver.create_market(market_info);
}

#[test]
#[should_panic(expected: ('OwnerNull',))]
fn test_create_market_with_null_owner_fails() {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/tests/solver/test_pause.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fn test_pause_allows_withdraws() {

// Withdraw.
start_prank(CheatTarget::One(solver.contract_address), alice());
solver.withdraw_at_ratio(market_id, shares);
solver.withdraw_public(market_id, shares);
}

#[test]
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/tests/solver/test_swap.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,24 @@ fn test_swap_fails_if_swap_sell_below_threshold_amount() {
};
solver.swap(market_id, params);
}

#[test]
#[should_panic(expected: ('NotSolver',))]
fn test_after_swap_fails_for_non_solver_caller() {
let (_base_token, _quote_token, _vault_token_class, solver, market_id, _vault_token_opt) =
before(
false
);

// Call after swap.
start_prank(CheatTarget::One(solver.contract_address), alice());
let solver_hooks = ISolverHooksDispatcher { contract_address: solver.contract_address };
let params = SwapParams {
is_buy: true,
amount: to_e18(10),
exact_input: true,
threshold_sqrt_price: Option::None(()),
threshold_amount: Option::None(()),
};
solver_hooks.after_swap(market_id, params);
}
Loading

0 comments on commit ae6ea21

Please sign in to comment.