Skip to content
Open
Show file tree
Hide file tree
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
71 changes: 45 additions & 26 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,26 +1,45 @@
AquaStorageTest:testDock1Token() (gas: 50947)
AquaStorageTest:testDock2Tokens() (gas: 80108)
AquaStorageTest:testDock3Tokens() (gas: 109363)
AquaStorageTest:testPullSingleSloadSstore() (gas: 67696)
AquaStorageTest:testPushSingleSloadSstore() (gas: 70616)
AquaStorageTest:testShip1Token() (gas: 46337)
AquaStorageTest:testShip2Tokens() (gas: 74317)
AquaStorageTest:testShip3Tokens() (gas: 102276)
AquaTest:testBalancesReturnsCorrectAmounts() (gas: 105159)
AquaTest:testBalancesReturnsZeroForNonExistentStrategy() (gas: 14848)
AquaTest:testBalancesReturnsZeroForTokenNotInStrategy() (gas: 50741)
AquaTest:testDockRequiresAllTokensFromShip() (gas: 76825)
AquaTest:testDockRequiresCorrectTokenCount() (gas: 79476)
AquaTest:testDockRequiresExactTokensFromShip() (gas: 81174)
AquaTest:testFullLifecycle() (gas: 144932)
AquaTest:testMultipleStrategiesSameTokens() (gas: 169969)
AquaTest:testPushFailsAfterDock() (gas: 54897)
AquaTest:testPushOnlyForShippedTokens() (gas: 54277)
AquaTest:testPushRequiresActiveStrategy() (gas: 21078)
AquaTest:testSafeBalancesReturnsCorrectAmountsForActiveStrategy() (gas: 75692)
AquaTest:testSafeBalancesRevertsAfterDock() (gas: 54832)
AquaTest:testSafeBalancesRevertsForNonExistentStrategy() (gas: 20785)
AquaTest:testSafeBalancesRevertsIfAnyTokenNotInStrategy() (gas: 79974)
AquaTest:testSafeBalancesTracksChangesFromPushPull() (gas: 138495)
AquaTest:testShipCannotBeCalledTwiceForSameStrategy() (gas: 52784)
AquaTest:testShipCannotHaveDuplicateTokens() (gas: 46793)
AquaHooksTest:testAfterShipRevertsPropagatesToAqua() (gas: 72625)
AquaHooksTest:testBeforeShipReturnsFalseReverts() (gas: 45643)
AquaHooksTest:testBeforeShipRevertsPropagatesToAqua() (gas: 44349)
AquaHooksTest:testBeforeShipWrapsETH() (gas: 199696)
AquaHooksTest:testETHSentRequiresBeforeHook() (gas: 24263)
AquaHooksTest:testETHSentWithAfterHookOnlyFails() (gas: 24043)
AquaHooksTest:testETHWithBothHooksSucceeds() (gas: 202975)
AquaHooksTest:testETHWithMultipleTokens() (gas: 230487)
AquaHooksTest:testHookConstants() (gas: 7841)
AquaHooksTest:testHooksReceiveCorrectStrategyHash() (gas: 106159)
AquaHooksTest:testNonHookAppWithHooksFails() (gas: 20950)
AquaHooksTest:testNonHookAppWithNoHooksSucceeds() (gas: 46710)
AquaHooksTest:testReentrancyFromAfterShipIsBlocked() (gas: 58456)
AquaHooksTest:testReentrancyFromBeforeShipIsBlocked() (gas: 58458)
AquaHooksTest:testReentrancyFromBothHooksIsBlocked() (gas: 62047)
AquaHooksTest:testShipWithAfterHookOnly() (gas: 103558)
AquaHooksTest:testShipWithBeforeHookOnly() (gas: 105031)
AquaHooksTest:testShipWithBothHooks() (gas: 107501)
AquaHooksTest:testShipWithNoHooks() (gas: 50752)
AquaStorageTest:testDock1Token() (gas: 51407)
AquaStorageTest:testDock2Tokens() (gas: 80579)
AquaStorageTest:testDock3Tokens() (gas: 109845)
AquaStorageTest:testPullSingleSloadSstore() (gas: 68210)
AquaStorageTest:testPushSingleSloadSstore() (gas: 71095)
AquaStorageTest:testShip1Token() (gas: 46745)
AquaStorageTest:testShip2Tokens() (gas: 74731)
AquaStorageTest:testShip3Tokens() (gas: 102696)
AquaTest:testBalancesReturnsCorrectAmounts() (gas: 105840)
AquaTest:testBalancesReturnsZeroForNonExistentStrategy() (gas: 14934)
AquaTest:testBalancesReturnsZeroForTokenNotInStrategy() (gas: 51238)
AquaTest:testDockRequiresAllTokensFromShip() (gas: 77287)
AquaTest:testDockRequiresCorrectTokenCount() (gas: 79938)
AquaTest:testDockRequiresExactTokensFromShip() (gas: 81641)
AquaTest:testFullLifecycle() (gas: 145978)
AquaTest:testMultipleStrategiesSameTokens() (gas: 171442)
AquaTest:testPushFailsAfterDock() (gas: 55409)
AquaTest:testPushOnlyForShippedTokens() (gas: 54738)
AquaTest:testPushRequiresActiveStrategy() (gas: 21131)
AquaTest:testSafeBalancesReturnsCorrectAmountsForActiveStrategy() (gas: 76204)
AquaTest:testSafeBalancesRevertsAfterDock() (gas: 55389)
AquaTest:testSafeBalancesRevertsForNonExistentStrategy() (gas: 20883)
AquaTest:testSafeBalancesRevertsIfAnyTokenNotInStrategy() (gas: 80484)
AquaTest:testSafeBalancesTracksChangesFromPushPull() (gas: 139186)
AquaTest:testShipCannotBeCalledTwiceForSameStrategy() (gas: 53461)
AquaTest:testShipCannotHaveDuplicateTokens() (gas: 47071)
56 changes: 54 additions & 2 deletions src/Aqua.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@ pragma solidity 0.8.30;

import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { SafeERC20, IERC20 } from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol";
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";

import { IAqua } from "./interfaces/IAqua.sol";
import { IShipHook } from "./interfaces/IShipHook.sol";
import { Balance, BalanceLib } from "./libs/Balance.sol";

/// @title Aqua - Shared Liquidity Layer
contract Aqua is IAqua {
contract Aqua is IAqua, ReentrancyGuardTransient {
using SafeERC20 for IERC20;
using SafeCast for uint256;
using BalanceLib for Balance;

uint8 private constant _DOCKED = 0xff;

/// @notice Hook flags for optional ship lifecycle hooks
uint8 public constant HOOK_NONE = 0x00;
uint8 public constant HOOK_BEFORE = 0x01;
uint8 public constant HOOK_AFTER = 0x02;
uint8 public constant HOOK_BOTH = 0x03;

mapping(address maker =>
mapping(address app =>
mapping(bytes32 strategyHash =>
Expand All @@ -37,18 +45,62 @@ contract Aqua is IAqua {
balance1 = amount1;
}

function ship(address app, bytes calldata strategy, address[] calldata tokens, uint256[] calldata amounts) external returns(bytes32 strategyHash) {
function ship(
address app,
bytes calldata strategy,
address[] calldata tokens,
uint256[] calldata amounts,
uint8 hooks
) external payable nonReentrant returns(bytes32 strategyHash) {
strategyHash = keccak256(strategy);
uint8 tokensCount = tokens.length.toUint8();
require(tokensCount != _DOCKED, MaxNumberOfTokensExceeded(tokensCount, _DOCKED));

// If ETH is sent, HOOK_BEFORE must be set
if (msg.value > 0) {
require((hooks & HOOK_BEFORE) != 0, ETHSentWithoutBeforeHook());
}

// beforeShip hook: called BEFORE balance storage (if HOOK_BEFORE flag set)
// Use for: ETH wrapping, pre-validation, setup
// Note: If app doesn't implement IShipHook, this will revert. This is intentional -
// apps should only set HOOK_BEFORE if they implement the hook. No ERC-165 check
// is performed to save gas (~2600 gas saved per call).
if ((hooks & HOOK_BEFORE) != 0) {
bool success = IShipHook(app).beforeShip{value: msg.value}(
msg.sender,
strategyHash,
tokens,
amounts
);
// beforeShip returns bool to allow graceful failure signaling.
// Returning false triggers ShipHookFailed; reverting propagates the error.
require(success, ShipHookFailed(app, HOOK_BEFORE));
}

// Core ship logic: store balances
emit Shipped(msg.sender, app, strategyHash, strategy);
for (uint256 i = 0; i < tokens.length; i++) {
Balance storage balance = _balances[msg.sender][app][strategyHash][tokens[i]];
require(balance.tokensCount == 0, StrategiesMustBeImmutable(app, strategyHash));
balance.store(amounts[i].toUint248(), tokensCount);
emit Pushed(msg.sender, app, strategyHash, tokens[i], amounts[i]);
}

// afterShip hook: called AFTER balance storage (if HOOK_AFTER flag set)
// Use for: notifications, additional state setup, external calls
// Note: Unlike beforeShip, afterShip has no return value. This is intentional:
// - afterShip is for side effects, not validation
// - If it fails, it should revert (consistent with callback patterns)
// - Any revert here will roll back the entire transaction including balance storage
if ((hooks & HOOK_AFTER) != 0) {
IShipHook(app).afterShip(
msg.sender,
strategyHash,
tokens,
amounts
);
}
}

function dock(address app, bytes32 strategyHash, address[] calldata tokens) external {
Expand Down
18 changes: 16 additions & 2 deletions src/interfaces/IAqua.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ interface IAqua {
/// @param token The token being queried
error SafeBalancesForTokenNotInActiveStrategy(address maker, address app, bytes32 strategyHash, address token);

/// @notice Thrown when a ship hook fails
/// @param app The app address that failed the hook
/// @param hookType 1 = beforeShip, 2 = afterShip
error ShipHookFailed(address app, uint8 hookType);

/// @notice Thrown when ETH is sent but HOOK_BEFORE flag is not set
error ETHSentWithoutBeforeHook();

/// @notice Emitted when a new strategy is shipped (deployed) and initialized with balances
/// @param maker The address of the maker shipping the strategy
/// @param app The app address associated with the strategy
Expand Down Expand Up @@ -89,16 +97,22 @@ interface IAqua {

/// @notice Ships a new strategy as of an app and sets initial balances
/// @dev Parameter `strategy` is presented fully instead of being pre-hashed for data availability
/// Hooks are optional and controlled via the `hooks` flags parameter:
/// - HOOK_BEFORE (0x01): Call beforeShip before balance storage (required if ETH sent)
/// - HOOK_AFTER (0x02): Call afterShip after balance storage
/// - HOOK_BOTH (0x03): Call both hooks
/// @param app The implementation contract
/// @param strategy Initialization data passed to the strategy
/// @param tokens Array of token addresses to approve
/// @param amounts Array of balance amounts for each token
/// @param hooks Bitmask of hooks to call (0x00=none, 0x01=before, 0x02=after, 0x03=both)
function ship(
address app,
bytes calldata strategy,
address[] calldata tokens,
uint256[] calldata amounts
) external returns(bytes32 strategyHash);
uint256[] calldata amounts,
uint8 hooks
) external payable returns(bytes32 strategyHash);

/// @notice Docks (deactivates) a strategy by clearing balances for specified tokens
/// @dev Sets balances to 0 for all specified tokens
Expand Down
96 changes: 96 additions & 0 deletions src/interfaces/IShipHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: LicenseRef-Degensoft-Aqua-Source-1.1
pragma solidity ^0.8.0;

/// @custom:license-url https://github.com/1inch/aqua/blob/main/LICENSES/Aqua-Source-1.1.txt
/// @custom:copyright © 2025 Degensoft Ltd

/// @title IShipHook
/// @notice Hook interface for apps to handle ship lifecycle operations
/// @dev Apps can implement these hooks to handle:
/// - beforeShip: Native ETH wrapping, pre-validation, setup
/// - afterShip: Notifications, additional state setup, external calls
///
/// ## Hook Flags
/// Hooks are optional and controlled via the `hooks` flags parameter in ship():
/// - HOOK_NONE (0x00): No hooks called
/// - HOOK_BEFORE (0x01): Call beforeShip before balance storage
/// - HOOK_AFTER (0x02): Call afterShip after balance storage
/// - HOOK_BOTH (0x03): Call both hooks
///
/// ## Interface Validation
/// Aqua does NOT verify that the app implements IShipHook before calling hooks.
/// If an app address is used with hooks enabled but doesn't implement IShipHook,
/// the call will revert. This is intentional to avoid gas overhead of ERC-165 checks.
/// Apps should only set hook flags if they implement the corresponding hook methods.
///
/// ## Error Handling
/// - beforeShip: MUST return true on success. Returning false or reverting will
/// cause the entire ship() transaction to revert. Use this for critical validation.
/// - afterShip: Any revert will propagate and cause ship() to fail. Since afterShip
/// is called AFTER balances are stored, apps should handle errors gracefully
/// if the hook failure shouldn't block the ship operation.
///
/// ## When to Return False vs Revert in beforeShip
/// - Return false: For expected failures that should block ship with a clear error
/// - Revert with custom error: For unexpected failures with detailed error info
/// Both will cause ship() to revert, but returning false uses ShipHookFailed error.
interface IShipHook {
/// @notice Called by Aqua BEFORE processing ship (when HOOK_BEFORE flag is set)
/// @dev Use for: ETH wrapping, pre-validation, setup
///
/// ## ETH Handling
/// The hook receives ETH via msg.value if sent with ship(). For ETH wrapping:
/// 1. Wrap ETH to WETH: WETH.deposit{value: msg.value}()
/// 2. Transfer WETH to maker: WETH.transfer(maker, msg.value)
/// This allows the maker to have WETH for the strategy without pre-wrapping.
///
/// ## Return Value
/// - Return true: Hook succeeded, continue with ship
/// - Return false: Hook failed, revert ship with ShipHookFailed error
/// - Revert: Propagates to ship() caller
///
/// ## Security
/// This hook is called BEFORE balances are stored. Any state changes made here
/// will be reverted if the ship fails. The hook is protected by reentrancy guard.
///
/// @param maker The maker address who is shipping the strategy
/// @param strategyHash The hash of the strategy being shipped (keccak256 of strategy bytes)
/// @param tokens Array of token addresses in the strategy
/// @param amounts Array of amounts for each token
/// @return success True if hook processed successfully, false to revert ship
function beforeShip(
address maker,
bytes32 strategyHash,
address[] calldata tokens,
uint256[] calldata amounts
) external payable returns (bool success);

/// @notice Called by Aqua AFTER ship is completed (when HOOK_AFTER flag is set)
/// @dev Use for: notifications, additional state setup, external calls
///
/// ## Execution Context
/// Called AFTER balances are stored, so strategy is already active in Aqua.
/// If this hook reverts, the entire ship() transaction reverts, including
/// the balance storage. Apps should handle non-critical failures gracefully.
///
/// ## No Return Value
/// Unlike beforeShip, afterShip has no return value. This is intentional:
/// - afterShip is for side effects (notifications, external calls)
/// - If it fails, it should revert (no silent failures)
/// - The pattern matches common callback patterns (no boolean dance)
///
/// ## Security
/// This hook is called AFTER state changes but still within the reentrancy guard.
/// Reentering ship() from this hook will revert.
///
/// @param maker The maker address who shipped the strategy
/// @param strategyHash The hash of the strategy that was shipped
/// @param tokens Array of token addresses in the strategy
/// @param amounts Array of amounts for each token
function afterShip(
address maker,
bytes32 strategyHash,
address[] calldata tokens,
uint256[] calldata amounts
) external;
}
Loading