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
7 changes: 1 addition & 6 deletions src/Aqua.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ contract Aqua is IAqua {
using SafeCast for uint256;
using BalanceLib for Balance;

error MaxNumberOfTokensExceeded(uint256 tokensCount, uint256 maxTokensCount);
error StrategiesMustBeImmutable(address app, bytes32 strategyHash);
error DockingShouldCloseAllTokens(address app, bytes32 strategyHash);
error PushToNonActiveStrategyPrevented(address maker, address app, bytes32 strategyHash, address token);
error SafeBalancesForTokenNotInActiveStrategy(address maker, address app, bytes32 strategyHash, address token);

uint8 private constant _DOCKED = 0xff;

mapping(address maker =>
Expand Down Expand Up @@ -58,6 +52,7 @@ contract Aqua is IAqua {
}

function dock(address app, bytes32 strategyHash, address[] calldata tokens) external {
require(tokens.length > 0, EmptyTokensArray());
for (uint256 i = 0; i < tokens.length; i++) {
Balance storage balance = _balances[msg.sender][app][strategyHash][tokens[i]];
require(balance.tokensCount == tokens.length, DockingShouldCloseAllTokens(app, strategyHash));
Expand Down
46 changes: 33 additions & 13 deletions src/interfaces/IAqua.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,42 @@ pragma solidity ^0.8.0;
/// @title Aqua - Shared Liquidity Layer
/// @notice Manages token balances (aka allowances) between makers (liquidity providers) and apps,
/// enabling shared liquidity access directly from maker wallets
/// @dev An "app" is an implementation contract that contains strategy logic and can pull tokens from makers
interface IAqua {
/// @notice Thrown when attempting to dock with an empty tokens array
error EmptyTokensArray();

/// @notice Thrown when attempting to ship a strategy with more than 254 tokens
error MaxNumberOfTokensExceeded(uint256 tokensCount, uint256 maxTokensCount);

/// @notice Thrown when attempting to ship a strategy that has already been shipped
error StrategiesMustBeImmutable(address app, bytes32 strategyHash);

/// @notice Thrown when docking with incorrect number of tokens
error DockingShouldCloseAllTokens(address app, bytes32 strategyHash);

/// @notice Thrown when pushing to a non-active or docked strategy
error PushToNonActiveStrategyPrevented(address maker, address app, bytes32 strategyHash, address token);

/// @notice Thrown when querying safe balances for a token not in the active strategy
error SafeBalancesForTokenNotInActiveStrategy(address maker, address app, bytes32 strategyHash, address token);

/// @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 strategy address being revoked
/// @param app The app address being shipped
/// @param strategyHash The hash of the strategy being shipped
/// @param strategy The strategy being shipped (abi enocoded)
/// @param strategy The strategy being shipped (abi encoded)
event Shipped(address maker, address app, bytes32 strategyHash, bytes strategy);

/// @notice Emitted when a maker revokes (deactivates) a strategy
/// @param maker The address of the maker revoking the strategy
/// @param app The strategy address being revoked
/// @param app The app address being revoked
/// @param strategyHash The hash of the strategy being revoked
event Docked(address maker, address app, bytes32 strategyHash);

/// @notice Emitted when a strategy pulls tokens from a maker
/// @param maker The address of the maker whose tokens are being pulled
/// @param app The strategy address that pulled the tokens
/// @param app The app address that pulled the tokens
/// @param strategyHash The hash of the strategy being pulled from
/// @param token The token address being pulled
/// @param amount The amount of tokens being pulled
Expand All @@ -32,7 +51,7 @@ interface IAqua {

/// @notice Emitted when tokens are pushed into a maker's balance
/// @param maker The address of the maker whose balance receives the tokens
/// @param app The strategy that gets increased balance
/// @param app The app whose balance is increased
/// @param strategyHash The hash of the strategy being pushed to
/// @param token The token address being pushed
/// @param amount The amount of tokens being pushed and added to the strategy's balance
Expand All @@ -41,16 +60,17 @@ interface IAqua {

/// @notice Returns the balance amount for a specific maker, app, and token combination
/// @param maker The address of the maker who granted the balance
/// @param app The address of the app/strategy that can pull tokens
/// @param app The address of the app that can pull tokens
/// @param strategyHash The hash of the strategy being used
/// @param token The address of the token
/// @return balance The current balance amount
/// @return tokensCount The number of tokens in the strategy
function rawBalances(address maker, address app, bytes32 strategyHash, address token) external view returns (uint248 balance, uint8 tokensCount);

/// @notice Returns balances of multiple tokens in a strategy, reverts if any of the tokens is not part of the active strategy
/// @notice Returns balances of multiple tokens in a strategy
/// @dev Reverts with SafeBalancesForTokenNotInActiveStrategy if any token is not part of an active strategy
/// @param maker The address of the maker who granted the balances
/// @param app The address of the app/strategy that can pull tokens
/// @param app The address of the app that can pull tokens
/// @param strategyHash The hash of the strategy being used
/// @param token0 The address of the first token
/// @param token1 The address of the second token
Expand All @@ -60,7 +80,7 @@ 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
/// @param app The implementation contract
/// @param app The app 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
Expand All @@ -72,10 +92,10 @@ interface IAqua {
) external returns(bytes32 strategyHash);

/// @notice Docks (deactivates) a strategy by clearing balances for specified tokens
/// @dev Sets balances to 0 for all specified tokens
/// @param app The strategy address to dock
/// @dev Sets balances to 0 for all specified tokens. Reverts with EmptyTokensArray if tokens array is empty.
/// @param app The app address to dock
/// @param strategyHash The hash of the strategy being docked
/// @param tokens Array of token addresses to clear
/// @param tokens Array of token addresses to clear (must not be empty)
function dock(address app, bytes32 strategyHash, address[] calldata tokens) external;

/// @notice Allows a strategy to pull tokens from a maker's wallet
Expand All @@ -90,7 +110,7 @@ interface IAqua {
/// @notice Pushes tokens and increases an app balance
/// @dev Transfers tokens from caller to maker and increases the app's balance
/// @param maker The maker whose balance receives the tokens
/// @param app The address of the app/strategy receiving the tokens
/// @param app The address of the app receiving the tokens
/// @param strategyHash The hash of the strategy being pushed
/// @param token The token address to push
/// @param amount The amount to push and add to balance
Expand Down
20 changes: 18 additions & 2 deletions src/libs/Balance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@ 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 Balance Struct
/// @notice Represents a maker's balance allocation for a specific token in a strategy
/// @dev Packed into a single storage slot: 248 bits for amount (sufficient for token balances) and 8 bits for tokensCount
struct Balance {
/// @notice The token balance amount
uint248 amount;
/// @notice The number of tokens in the strategy (0xff indicates docked/inactive)
uint8 tokensCount;
}

/// @title Balance Library
/// @notice Library for efficient storage operations on Balance structs
/// @dev Uses inline assembly to ensure single SLOAD/SSTORE operations for gas efficiency
library BalanceLib {
/// @dev Assembly implementation to make sure exactly 1 SLOAD is being used
/// @notice Loads a Balance from storage
/// @dev Uses assembly to ensure exactly 1 SLOAD is performed
/// @param balance The storage pointer to the Balance struct
/// @return amount The token balance amount
/// @return tokensCount The number of tokens in the strategy
function load(Balance storage balance) internal view returns (uint248 amount, uint8 tokensCount) {
assembly ("memory-safe") {
let packed := sload(balance.slot)
Expand All @@ -19,7 +31,11 @@ library BalanceLib {
}
}

/// @dev Assembly implementation to make sure exactly 1 SSTORE is being used
/// @notice Stores a Balance to storage
/// @dev Uses assembly to ensure exactly 1 SSTORE is performed
/// @param balance The storage pointer to the Balance struct
/// @param amount The token balance amount to store
/// @param tokensCount The number of tokens in the strategy
function store(Balance storage balance, uint248 amount, uint8 tokensCount) internal {
assembly ("memory-safe") {
let packed := or(amount, shl(248, tokensCount))
Expand Down
6 changes: 6 additions & 0 deletions src/libs/Multicall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ pragma solidity 0.8.30;
/// @custom:license-url https://github.com/1inch/aqua/blob/main/LICENSES/Aqua-Source-1.1.txt
/// @custom:copyright © 2025 Degensoft Ltd

/// @title Multicall - Batch Execution Utility
/// @notice Allows batching multiple calls to this contract via delegatecall for atomic operations
/// @dev Intended for inheritance. Be cautious of recursion in batched calldata.
contract Multicall {
/// @notice Execute multiple calls in a single transaction
/// @dev Each call is executed via delegatecall. If any call fails, the entire transaction reverts.
/// @param data Array of encoded function calls to execute
function multicall(bytes[] calldata data) external {
for (uint256 i = 0; i < data.length; i++) {
(bool success,) = address(this).delegatecall(data[i]);
Expand Down
6 changes: 4 additions & 2 deletions src/libs/ReentrancyGuard.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// SPDX-License-Identifier: LicenseRef-Degensoft-Aqua-Source-1.1
pragma solidity ^0.8.0; // tload/tstore are available since 0.8.24
pragma solidity ^0.8.24;

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

import { TransientLock, TransientLockLib } from "./TransientLock.sol";

/// @dev Base contract with reentrancy guard functionality using transient storage locks.
/// @title ReentrancyGuard - Transient Storage-Based Protection
/// @notice Abstract contract providing reentrancy guards using transient storage for gas efficiency.
/// @dev Requires Solidity >=0.8.24 for transient storage support (tload/tstore operations).
///
/// Use private _lock defined in this contract:
/// ```solidity
Expand Down
25 changes: 13 additions & 12 deletions test/Aqua.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "forge-std/Test.sol";
import { dynamic } from "./utils/Dynamic.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "src/Aqua.sol";
import "src/interfaces/IAqua.sol";

contract MockToken is ERC20 {
constructor(string memory name) ERC20(name, "MOCK") {
Expand Down Expand Up @@ -62,7 +63,7 @@ contract AquaTest is Test {

// Try to ship again with same strategy
vm.prank(maker);
vm.expectRevert(abi.encodeWithSelector(Aqua.StrategiesMustBeImmutable.selector, app, keccak256("strategy1")));
vm.expectRevert(abi.encodeWithSelector(IAqua.StrategiesMustBeImmutable.selector, app, keccak256("strategy1")));
aqua.ship(
app,
"strategy1",
Expand All @@ -75,7 +76,7 @@ contract AquaTest is Test {
// The contract prevents duplicate tokens in the same ship call
// because it checks tokensCount == 0 for each token
vm.prank(maker);
vm.expectRevert(abi.encodeWithSelector(Aqua.StrategiesMustBeImmutable.selector, app, keccak256("strategy_dup")));
vm.expectRevert(abi.encodeWithSelector(IAqua.StrategiesMustBeImmutable.selector, app, keccak256("strategy_dup")));
aqua.ship(
app,
"strategy_dup",
Expand All @@ -98,7 +99,7 @@ contract AquaTest is Test {

// Try to dock with only 1 token
vm.prank(maker);
vm.expectRevert(abi.encodeWithSelector(Aqua.DockingShouldCloseAllTokens.selector, app, keccak256("strategy2")));
vm.expectRevert(abi.encodeWithSelector(IAqua.DockingShouldCloseAllTokens.selector, app, keccak256("strategy2")));
aqua.dock(
app,
keccak256("strategy2"),
Expand All @@ -118,7 +119,7 @@ contract AquaTest is Test {

// Try to dock with different token
vm.prank(maker);
vm.expectRevert(abi.encodeWithSelector(Aqua.DockingShouldCloseAllTokens.selector, app, keccak256("strategy3")));
vm.expectRevert(abi.encodeWithSelector(IAqua.DockingShouldCloseAllTokens.selector, app, keccak256("strategy3")));
aqua.dock(
app,
keccak256("strategy3"),
Expand All @@ -138,7 +139,7 @@ contract AquaTest is Test {

// Try to dock with 3 tokens
vm.prank(maker);
vm.expectRevert(abi.encodeWithSelector(Aqua.DockingShouldCloseAllTokens.selector, app, keccak256("strategy4")));
vm.expectRevert(abi.encodeWithSelector(IAqua.DockingShouldCloseAllTokens.selector, app, keccak256("strategy4")));
aqua.dock(
app,
keccak256("strategy4"),
Expand All @@ -151,7 +152,7 @@ contract AquaTest is Test {
function testPushRequiresActiveStrategy() public {
// Try to push without ship
vm.prank(pusher);
vm.expectRevert(abi.encodeWithSelector(Aqua.PushToNonActiveStrategyPrevented.selector, maker, app, keccak256("nonexistent"), address(token1)));
vm.expectRevert(abi.encodeWithSelector(IAqua.PushToNonActiveStrategyPrevented.selector, maker, app, keccak256("nonexistent"), address(token1)));
aqua.push(maker, app, keccak256("nonexistent"), address(token1), 100e18);
}

Expand All @@ -174,7 +175,7 @@ contract AquaTest is Test {

// Try to push after dock
vm.prank(pusher);
vm.expectRevert(abi.encodeWithSelector(Aqua.PushToNonActiveStrategyPrevented.selector, maker, app, keccak256("strategy5"), address(token1)));
vm.expectRevert(abi.encodeWithSelector(IAqua.PushToNonActiveStrategyPrevented.selector, maker, app, keccak256("strategy5"), address(token1)));
aqua.push(maker, app, keccak256("strategy5"), address(token1), 50e18);
}

Expand All @@ -190,7 +191,7 @@ contract AquaTest is Test {

// Try to push token2 (not shipped)
vm.prank(pusher);
vm.expectRevert(abi.encodeWithSelector(Aqua.PushToNonActiveStrategyPrevented.selector, maker, app, keccak256("strategy6"), address(token2)));
vm.expectRevert(abi.encodeWithSelector(IAqua.PushToNonActiveStrategyPrevented.selector, maker, app, keccak256("strategy6"), address(token2)));
aqua.push(maker, app, keccak256("strategy6"), address(token2), 50e18);
}

Expand Down Expand Up @@ -231,7 +232,7 @@ contract AquaTest is Test {

// 5. Verify can't push after dock
vm.prank(pusher);
vm.expectRevert(abi.encodeWithSelector(Aqua.PushToNonActiveStrategyPrevented.selector, maker, app, strategyHash, address(token1)));
vm.expectRevert(abi.encodeWithSelector(IAqua.PushToNonActiveStrategyPrevented.selector, maker, app, strategyHash, address(token1)));
aqua.push(maker, app, strategyHash, address(token1), 10e18);

// 6. Verify balances are zero after dock
Expand Down Expand Up @@ -364,7 +365,7 @@ contract AquaTest is Test {
// Try to query safeBalances for non-existent strategy
vm.expectRevert(
abi.encodeWithSelector(
Aqua.SafeBalancesForTokenNotInActiveStrategy.selector,
IAqua.SafeBalancesForTokenNotInActiveStrategy.selector,
maker,
app,
keccak256("nonexistent"),
Expand Down Expand Up @@ -395,7 +396,7 @@ contract AquaTest is Test {
// Try to query with token3 (not in strategy)
vm.expectRevert(
abi.encodeWithSelector(
Aqua.SafeBalancesForTokenNotInActiveStrategy.selector,
IAqua.SafeBalancesForTokenNotInActiveStrategy.selector,
maker,
app,
strategyHash,
Expand Down Expand Up @@ -434,7 +435,7 @@ contract AquaTest is Test {
// Try to query safeBalances after dock
vm.expectRevert(
abi.encodeWithSelector(
Aqua.SafeBalancesForTokenNotInActiveStrategy.selector,
IAqua.SafeBalancesForTokenNotInActiveStrategy.selector,
maker,
app,
strategyHash,
Expand Down