Table of Contents
- Table of Contents
- Summary
- Scope
- System Overview
- Security Model and Trust Assumptions
- Medium Severity
- Low Severity
- Notes & Additional Information
- transfer_ownership Performs Immediate Ownership Transfer Without Safeguards
- Ambiguous Use of "Owner" in Whitelist Program May Cause Confusion
- Misleading Underscore Prefix on Used Argument _new_owner
- Redundant .key() Call on a Pubkey Value
- Programs Use Outdated Anchor Dependencies
- The toolchain Section in Anchor.toml Is Empty
- Program File Contains Too Many Lines of Code
- Incorrect Docstrings
- Conclusion
Summary
- Type
- Defi
- Timeline
- From 2025-03-31
- To 2025-04-08
- Languages
- Rust
- Total Issues
- 11 (6 resolved, 1 partially resolved)
- Critical Severity Issues
- 0 (0 resolved)
- High Severity Issues
- 0 (0 resolved)
- Medium Severity Issues
- 1 (0 resolved)
- Low Severity Issues
- 2 (0 resolved, 1 partially resolved)
- Notes & Additional Information
- 8 (6 resolved)
Scope
OpenZeppelin audited the 1inch/solana-fusion-protocol repository at commit 8743d38.
In scope were the following files:
programs
├── fusion-swap
│ └── src
│ ├── auction.rs
│ ├── error.rs
│ └── lib.rs
└── whitelist
└── src
├── error.rs
└── lib.rs
System Overview
1inch Fusion is a decentralized exchange that facilitates token swaps while being resistant to front-running, leveraging a Dutch auction mechanism. Instead of having a public, open order book, the system utilizes an escrow-based approach where users (makers
) create specific swap orders. These orders define the source asset and amount to be sold, along with the minimum amount of the destination asset that they are willing to receive.
The system employs a time-decaying exchange rate, being more favorable to the maker at the beginning and then gradually decreasing towards their minimum acceptable rate over the order's duration. A network of whitelisted actors (resolvers
or takers
) monitor these orders and compete to fill them when the rate becomes profitable according to their strategies. This design aims to protect users from MEV activities like sandwich attacks commonly encountered in public AMM pools.
The system comprises two main Solana programs:
fusion_swap
(5uzpYuGqBaetRMXPDtGWGN9W4mdmgBzpGHcQACrZ1npi
): Handles the core logic of order creation, escrow management, order filling, and cancellation.whitelist
(DyXFcRxGWFoMz1j76SeMXHjQqZKudLXeJY3h1K7BNJiQ
): Manages the set of authorized addresses (resolvers) permitted to fill or cancel expired orders within thefusion_swap
program.
Order lifetime
The lifecycle of a swap order involves several potential paths.
- Creation: A user (
maker
) initiates the process by calling thecreate
instruction. They provide theOrderConfig
, which includes:- Source token and amount (
src_amount
) - Destination token and the minimum acceptable amount (
min_dst_amount
) - An estimated destination amount (
estimated_dst_amount
), used for surplus fee calculation - Order expiration timestamp (
expiration_time
) - Details for the Dutch auction (
dutch_auction_data
). - Fee configuration (
fee
), including potential protocol, integrator, and cancellation fees - Flags indicating whether source/destination assets are native SOL (
src_asset_is_native
,dst_asset_is_native
)
- Source token and amount (
The maker
signs the transaction, and their specified src_amount
in SPL tokens or SOL is sent to a dedicated escrow token account (an Associated Token Account - ATA). In case of SOL it is wrapped into wSOL. The ATA is owned by a Program Derived Address (PDA) unique to the order details (escrow
account), with the PDA as its authority. Various checks ensure order validity (e.g., non-zero amounts, valid expiration, consistent fee configs).
-
Filling: A whitelisted
taker
(resolver) finds a suitable order and calls thefill
instruction.- The taker specifies the order details (
OrderConfig
) and theamount
of the source token they wish to purchase (allowing partial fills). - The system verifies that the order has not expired and that the taker is whitelisted (possesses a valid
ResolverAccess
account from thewhitelist
program). - It calculates the required
dst_amount
the taker must pay, factoring in the Dutch auction rate adjustment based on the current time usingcalculate_rate_bump
. - The specified
amount
of source tokens is transferred from theescrow_src_ata
to thetaker_src_ata
. - The calculated
dst_amount
is paid by the taker. This amount is then divided as follows:- Integrator fee goes to the
integrator_dst_acc
(if applicable). - Protocol fee (including any surplus fee) goes to the
protocol_dst_acc
(if applicable). - The remaining amount goes to the
maker_receiver
(or theirmaker_dst_ata
).
- Integrator fee goes to the
- If the fill completes the entire
src_amount
, theescrow_src_ata
is closed, and its lamport balance (rent) is returned to themaker
.
- The taker specifies the order details (
-
Cancellation by Maker: The original
maker
can decide to cancel their order by calling thecancel
instruction.- The maker provides the unique
order_hash
derived from the order parameters. - The instruction verifies that the
maker
is the signer. - Any remaining source tokens in
escrow_src_ata
are transferred back tomaker_src_ata
. escrow_src_ata
is closed, returning its lamport balance (rent) to themaker
.
- The maker provides the unique
-
Cancellation By Resolver: If an order expires, it becomes eligible for cancellation by a whitelisted resolver via the
cancel_by_resolver
instruction. This mechanism incentivizes cleaning up expired orders.- A whitelisted
resolver
calls the instruction, providing theOrderConfig
and areward_limit
they are willing to accept. - The system verifies that the order is expired and that the caller is whitelisted.
- The system checks if cancellation by resolvers is permitted.
- If the source asset was not native SOL, the remaining tokens are transferred back to the
maker_src_ata
. - A
cancellation_premium
is calculated based on the time elapsed since expiration (calculate_premium
), capped byorder.fee.max_cancellation_premium
. - The
escrow_src_ata
is closed. Crucially, its entire lamport balance (rent + any principal if the source was wrapped SOL) is transferred to theresolver
. - The
resolver
then immediately transfers a portion of these received lamports back to themaker
. The amount returned to the maker is the total received minus the calculatedcancellation_premium
(further capped by thereward_limit
provided by the resolver). The resolver keeps the premium as a reward.
- A whitelisted
Dutch Auction Implementation
The Dutch auction modifies the exchange rate over time, making the order progressively cheaper for the taker. It is implemented via the AuctionData
struct within the OrderConfig
and calculate_rate_bump
functions.
- Configuration:
AuctionData
contains:start_time
: The Unix timestamp when the auction begins.duration
: The total length of the auction period.initial_rate_bump
: A starting adjustment (in basis points,BASE_1E5
) applied to the destination amount. A positive bump means the taker initially pays more than the base rate derived frommin_dst_amount
.points_and_time_deltas
: A vector defining points on a curve. Each point specifies arate_bump
and thetime_delta
. This allows for defining custom decay curves.
- Calculation: The
calculate_rate_bump
function determines the applicable rate bump based on the currenttimestamp
:- Before
start_time
, theinitial_rate_bump
is used. - After
start_time + duration
, the rate bump is 0 (meaning the rate reverts to the one implied bymin_dst_amount
). - During the auction, the function iterates through the
points_and_time_deltas
. It finds the segment of the curve corresponding to the currenttimestamp
and performs linear interpolation between therate_bump
values of the segment's start and end points to find the current bump.
- Before
Fees
The protocol incorporates several types of fees, configured within the OrderConfig.fee
(FeeConfig
struct):
- Protocol Fee: A percentage (
protocol_fee
, basis points relative toBASE_1E5
) of the totaldst_amount
paid by the taker during afill
. This fee is directed to theprotocol_dst_acc
if provided. - Integrator Fee: A percentage (
integrator_fee
, basis points relative toBASE_1E5
) of the totaldst_amount
paid by the taker during afill
. This fee is directed to theintegrator_dst_acc
if provided, allowing UI providers or integrators to earn revenue. - Surplus Fee (Positive Slippage): If the actual amount the maker receives (after the protocol and integrator fees are deducted from
dst_amount
) exceeds theestimated_dst_amount
provided in the order, a percentage (surplus_percentage
, basis points relative toBASE_1E2
= 100%) of this positive difference (surplus) is taken as an additional fee. This surplus fee is added to the protocol fee amount. - Cancellation Premium: Configured via
max_cancellation_premium
(an absolute lamport amount). When a resolver cancels an expired order usingcancel_by_resolver
, they earn a premium calculated based on the time elapsed since expiration, capped by this value. This fee is effectively paid by the maker from the funds held in the escrow ATA (specifically its lamport balance).
Whitelist
The whitelist
program serves as an access control layer for specific actions within the fusion_swap
program.
- Purpose: It restricts the ability to act as a
taker
(callingfill
) and to cancel expired orders (callingcancel_by_resolver
) to only authorized addresses. This fulfills the "network of professional market makers" concept. - Mechanism:
- The program maintains a central
WhitelistState
account (a PDA seeded byWHITELIST_STATE_SEED
) which stores theowner
public key. - The
owner
has the authority to manage the whitelist. - To whitelist a user (resolver), the owner calls
register
, providing the user's public key. This creates an emptyResolverAccess
account (a PDA seeded byRESOLVER_ACCESS_SEED
and the user's key). The existence of this account signifies that the user is whitelisted. - To remove a user, the owner calls
deregister
, which closes the user'sResolverAccess
account. - The
fill
andcancel_by_resolver
instructions infusion_swap
include constraints that verify that the transaction signer (taker
orresolver
) has a validResolverAccess
account derived using thewhitelist
program's ID and the correct seeds.
- The program maintains a central
Security Model and Trust Assumptions
Users and participants in this protocol operate under several security assumptions and known risks:
- Whitelist Reliance: The security and liveness of the filling process depend entirely on the whitelisted resolvers. Users trust the
whitelist
program's owner to:- Only whitelist competent and non-malicious resolvers.
- Maintain a sufficient set of active resolvers to ensure that orders are filled.
- Not arbitrarily remove resolvers or transfer ownership to untrusted parties.
- Resolver Behavior: Users trust that whitelisted resolvers will act economically rationally and fill orders when profitable. There is no on-chain guarantee that an order will be filled, even if the rate becomes favorable.
-
Off-Chain Dependencies: Order discovery and potentially submission likely rely on off-chain infrastructure (e.g., APIs, front-ends like 1inch's). Users and resolvers trust this infrastructure to be available, accurate, and censorship-resistant. Downtime or manipulation of this layer can prevent order creation or filling. The centralized backend is critical for order flow. If it becomes unavailable or behaves incorrectly, valid orders might not be forwarded to takers, leading to degraded protocol usability.
Conversely, if the backend fails to correctly filter invalid orders, they may still be passed along, undermining the integrity of the off-chain matching process. The backend introduces a layer of trust that contradicts the trust-minimized ethos of decentralized systems. Users and takers must assume that the backend is not censoring or selectively delaying orders. Although the on-chain program uses PDA checks to ensure the integrity of orders at fill time, it cannot prevent the backend from influencing which orders are seen or prioritized.
-
Expiration Handling: Expired orders rely on resolvers calling
cancel_by_resolver
for cleanup, incentivized by the cancellation premium. If no resolver cancels, the funds (especially native SOL) remain in the escrow ATA until manually cancelled by the maker or eventually by a resolver. Users trust that this incentive mechanism is sufficient.
Privileged Roles
Several roles possess special capabilities within the system:
-
Whitelist Owner:
- Description: The single address designated as the owner in the
whitelist
program'sWhitelistState
account. - Capabilities:
- Add resolvers (
register
) - Remove resolvers (
deregister
) - Transfer ownership of the whitelist (
transfer_ownership
)
- Add resolvers (
- Description: The single address designated as the owner in the
-
Resolvers / Takers:
- Description: Addresses that have been whitelisted by the Whitelist Owner via the
register
function, resulting in the creation of a correspondingResolverAccess
PDA. - Capabilities (within
fusion_swap
):- Fill active orders (
fill
) - Cancel expired orders for a premium (
cancel_by_resolver
)
- Fill active orders (
- Description: Addresses that have been whitelisted by the Whitelist Owner via the
Medium Severity
Lack of Event Emission
None of the instructions in either program emit events upon execution. This omission hinders transparency and external observability. Without emitted events, off-chain consumers such as dashboards, indexers, and other smart contracts must rely on custom transaction-parsing logic to infer state changes like order creation, fulfillment, or cancellation. This increases implementation complexity and creates a brittle dependency on internal instruction formats.
Although transaction data is publicly available on-chain, the lack of standardized event emissions significantly reduces the ease of monitoring protocol activity. This design choice can impact the developer and user experience, as protocols that emit events allow for more accessible and standardized tracking of meaningful on-chain events.
Consider emitting events after sensitive changes take place to facilitate tracking and notify off-chain clients that are following the programs' activity.
Update: Acknowledged, not resolved. The 1inch team stated:
Adding events means increasing the transaction's CU which we cannot yet justify.
Low Severity
Lack of External Documentation
The solana-fusion-protocol
repository is missing fundamental project information, including a README.md
file, project description, and any form of documentation directory or usage guides. This significantly reduces the accessibility and maintainability of the codebase, especially for new contributors, auditors, and external developers attempting to understand or integrate with the protocol. The absence of a README.md
also means that there is neither clear guidance on how to set up, test, or deploy the project, nor any details on its purpose, architecture, or dependencies.
Including a basic README.md
with setup instructions, usage examples, and an overview of the protocol is a widely accepted best practice in open-source and production codebases. This ensures that the project can be reliably used and reviewed, and it also helps establish the credibility and usability of the code.
Consider at least adding the following to the documentation:
- A short description of the protocol
- Setup and build instructions
- How to run tests
- Contract architecture and module descriptions
Update: Partially resolved in pull request #72. The The 1inch team stated:
Noted. We added a whitepaper and we will add a basic README.md in future versions.
Missing Docstrings
Key parts of the fusion_swap
program lack essential docstrings, reducing clarity and increasing the risk of misuse:
- Program Module (
#[program]
): Missing top-level docstring explaining the contract's purpose and functionality. - Instruction Handlers (
create
,fill
,cancel
,cancel_by_resolver
): No documentation on purpose, parameters, expected preconditions, side effects, or error cases. OrderConfig
Struct: Lacks docstring for the struct and its fields (id
,src_amount
,min_dst_amount
,expiration_time
, etc.), which are central to order logic.UniTransferParams
Enum: No explanations for the enum or its variants (NativeTransfer
,TokenTransfer
), which abstract token transfers.- Helper Functions:
order_hash
,get_fee_amounts
, anduni_transfer
lack docstrings describing logic, parameters, and expected behavior.
Consider adding concise documentation to the aforementioned areas. Doing this would greatly improve maintainability, readability, and safety for users and auditors.
Update: Acknowledged, will resolve. The 1inch team stated:
Noted. We will add the essential documentation in future versions.
Notes & Additional Information
transfer_ownership
Performs Immediate Ownership Transfer Without Safeguards
The transfer_ownership
function in the whitelist program assigns ownership to the _new_owner
address immediately upon invocation. This approach introduces risk, as an incorrect or unintended address may be set as the new owner due to human error.
Without a confirmation mechanism, such as a two-step ownership transfer (e.g., proposeOwner
and acceptOwnership
), there is no opportunity to recover from a misconfiguration. If ownership is accidentally assigned to an unwanted contract address, a burn address, or an address not controlled by the intended recipient, the contract may become irreversibly inaccessible or mismanaged.
To mitigate this, consider implementing a two-step transfer pattern, where the new owner must explicitly accept ownership before the change is finalized. Alternatively, if there are guarantees in the system design that ensure safe use of the current one-step transfer mechanism, they should be clearly documented to justify the approach.
Update: Acknowledged, not resolved. The 1inch team stated:
In the unlikely event control over the whitelist contract is lost, redeployment to a new address would suffice without significantly disrupting protocol functionality. Hence, we do not see a strong need for additional checks.
Ambiguous Use of "Owner" in Whitelist Program May Cause Confusion
In the context of the whitelist
program, the term "owner" is used to refer to the actor who controls the whitelist state. However, this terminology can be misleading within the Solana ecosystem, as in this blockchain network, an account’s owner
is the program ID that has permission to modify the account's data. This is distinct from any authority or admin-like key that might control the behavior or state within a program.
This ambiguity can be particularly problematic for developers familiar with Ethereum, where "owner" often connotes a privileged user role rather than program ownership.
To improve clarity and maintain consistency with Solana conventions, consider using more precise terminology such as authority
, admin
, or controller
.
Update: Resolved in pull request #84.
Misleading Underscore Prefix on Used Argument _new_owner
The transfer_ownership
function takes _new_owner
as a parameter, which is used within the function body. However, the underscore prefix conventionally signals that a parameter is intentionally unused. This creates a misleading impression that _new_owner
is not used, potentially confusing readers or maintainers.
To improve code clarity and adhere to conventional naming practices, consider removing the underscore from _new_owner
.
Update: Resolved in pull request #83.
Redundant .key()
Call on a Pubkey
Value
In the transfer_ownership
function, the whitelist_state.owner = _new_owner.key();
assignment is redundant because _new_owner
is already a Pubkey
. The .key()
method is typically used on AccountInfo
objects to retrieve the Pubkey
. However, in this case, calling .key()
on a Pubkey
simply returns itself, adding unnecessary verbosity and potentially confusing readers.
Consider replacing the whitelist_state.owner = _new_owner.key();
assignment with whitelist_state.owner = _new_owner;
to make the code more concise and idiomatic.
Update: Resolved in pull request #82.
Programs Use Outdated Anchor Dependencies
Currently, the programs rely on outdated versions of both the anchor-lang
and anchor-spl
crates. Since their release, there is a new version containing bug fixes, improved developer ergonomics, and new features that may enhance the safety and maintainability of the codebase.
Using outdated dependencies can expose the program to known vulnerabilities or bugs already addressed in newer versions. It may also hinder the adoption of best practices and reduce compatibility with other up-to-date tooling in the ecosystem.
Consider upgrading to the latest versions of the anchor-lang
and anchor-spl
crates, ensuring that the changes introduced in the newer versions are compatible with the current codebase.
Update: Resolved in pull request #80.
The toolchain
Section in Anchor.toml
Is Empty
The Anchor.toml
file is missing a specified toolchain
version. Omitting this can lead to inconsistencies between the Anchor CLI version used by different developers/auditors and the one expected by the project. Version mismatches may introduce subtle bugs, compilation errors, or unexpected behavior, especially if breaking changes have been introduced in newer releases.
Defining the toolchain version helps ensure reproducibility of builds and a consistent development environment across teams and CI systems. It also improves clarity for auditors reviewing the code under a known Anchor version.
To mitigate this, specify the expected Anchor CLI version in the toolchain
section of Anchor.toml
.
Update: Resolved in pull request #81.
Program File Contains Too Many Lines of Code
The fusion_swap
program currently implements all instructions within a single, large file. This monolithic structure negatively impacts readability, collaboration, and future scalability.
Smaller, instruction-specific modules are easier to understand and navigate. When each instruction (e.g., create
and cancel
) and its associated components (such as its Accounts
struct) are located in separate files, developers can more easily comprehend and maintain the codebase. This modularization also reduces the likelihood of merge conflicts, particularly when multiple team members are working on different instructions concurrently. Moreover, as the program expands in size or complexity, the current flat structure may become increasingly difficult to manage, making debugging or refactoring more error-prone.
Consider splitting the fusion_swap
program by putting each instruction into a separate module or file. This would help bring clarity to the codebase, facilitate team collaboration, and improve long-term maintainability.
Update: Acknowledged, not resolved. The 1inch team stated:
Noted — we’ve decided not to proceed with changes at this time.
Incorrect Docstrings
Throughout the codebase, multiple instances of docstrings containing technically incorrect or misleading information were identified:
-
/// Account to store order conditions
(Present in lines 415, 501,577, and 638 infusion-swap/src/lib.rs
). This description inaccurately suggests that the escrow account stores order conditions. In reality, it is a PDA used as the authority for the escrow source token account. Its address is derived from order details (order_hash
), but it does not store the order configuration directly. A clearer description would be:/// PDA derived from order details, acting as the authority for the escrow ATA
-
/// size(timestamp) + size(rate_bump) < 64
(Present on lines 37 and 50 onauction.rs
). This statement is factually incorrect.timestamp
is au64
(64 bits), andrate_bump
, though originally au16
value, is cast tou64
for arithmetic purposes. Even considering the original type, the combined bit size is64 + 16 = 80
, which is not less than 64. The intention appears to be to justify that thetime_difference * rate_bump
multiplication cannot overflow au64
container. In practice, the values are constrained:- Time differences are derived from a
u16
(maximum 65535) value. rate_bump
values also originate from au16
value.
- Time differences are derived from a
Therefore, the maximum multiplication result is approximately 65535 * 65535 ≈ 2^32
, which fits safely within a u64
container. While the logic is sound, the justification based on bit sizes is flawed and should be clarified.
Clear and accurate docstrings are essential for correctness, maintainability, and auditability. As such, consider addressing the aforementioned instances of incorrect/misleading docstirngs.
Update: Resolved in pull request #85.
Conclusion
1inch Fusion is a decentralized exchange on the Solana blockchain that facilitates token swaps that are resistant to front-running, leveraging a Dutch auction mechanism. Instead of using a public order book, users create escrowed swap orders with defined minimum returns. The exchange rate decays over time (based on a Dutch auction model), starting favorably for the maker and dropping until a whitelisted actor accepts the trade. This setup helps prevent MEV attacks.
The implementation reflects a solid understanding of Solana development, with robust handling of edge cases, a comprehensive test suite, and thoughtful design choices. While no critical vulnerabilities were found, some minor issues were identified and actionable recommendations were provided to improve code maintainability, adherence to best practices, documentation, and overall clarity. The 1inch team is appreciated for being highly responsive and collaborative throughout the process.