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
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 the fusion_swap
program.The lifecycle of a swap order involves several potential paths.
maker
) initiates the process by calling the create
instruction. They provide the OrderConfig
, which includes:
src_amount
)min_dst_amount
)estimated_dst_amount
), used for surplus fee calculationexpiration_time
)dutch_auction_data
).fee
), including potential protocol, integrator, and cancellation feessrc_asset_is_native
, dst_asset_is_native
)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 the fill
instruction.
OrderConfig
) and the amount
of the source token they wish to purchase (allowing partial fills).ResolverAccess
account from the whitelist
program).dst_amount
the taker must pay, factoring in the Dutch auction rate adjustment based on the current time using calculate_rate_bump
.amount
of source tokens is transferred from the escrow_src_ata
to the taker_src_ata
.dst_amount
is paid by the taker. This amount is then divided as follows:
integrator_dst_acc
(if applicable).protocol_dst_acc
(if applicable).maker_receiver
(or their maker_dst_ata
).src_amount
, the escrow_src_ata
is closed, and its lamport balance (rent) is returned to the maker
.Cancellation by Maker: The original maker
can decide to cancel their order by calling the cancel
instruction.
order_hash
derived from the order parameters.maker
is the signer.escrow_src_ata
are transferred back to maker_src_ata
.escrow_src_ata
is closed, returning its lamport balance (rent) to the maker
.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.
resolver
calls the instruction, providing the OrderConfig
and a reward_limit
they are willing to accept.maker_src_ata
.cancellation_premium
is calculated based on the time elapsed since expiration (calculate_premium
), capped by order.fee.max_cancellation_premium
.escrow_src_ata
is closed. Crucially, its entire lamport balance (rent + any principal if the source was wrapped SOL) is transferred to the resolver
.resolver
then immediately transfers a portion of these received lamports back to the maker
. The amount returned to the maker is the total received minus the calculated cancellation_premium
(further capped by the reward_limit
provided by the resolver). The resolver keeps the premium as a reward.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.
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 from min_dst_amount
.points_and_time_deltas
: A vector defining points on a curve. Each point specifies a rate_bump
and the time_delta
. This allows for defining custom decay curves.calculate_rate_bump
function determines the applicable rate bump based on the current timestamp
:
start_time
, the initial_rate_bump
is used.start_time + duration
, the rate bump is 0 (meaning the rate reverts to the one implied by min_dst_amount
).points_and_time_deltas
. It finds the segment of the curve corresponding to the current timestamp
and performs linear interpolation between the rate_bump
values of the segment's start and end points to find the current bump.The protocol incorporates several types of fees, configured within the OrderConfig.fee
(FeeConfig
struct):
protocol_fee
, basis points relative to BASE_1E5
) of the total dst_amount
paid by the taker during a fill
. This fee is directed to the protocol_dst_acc
if provided.integrator_fee
, basis points relative to BASE_1E5
) of the total dst_amount
paid by the taker during a fill
. This fee is directed to the integrator_dst_acc
if provided, allowing UI providers or integrators to earn revenue.dst_amount
) exceeds the estimated_dst_amount
provided in the order, a percentage (surplus_percentage
, basis points relative to BASE_1E2
= 100%) of this positive difference (surplus) is taken as an additional fee. This surplus fee is added to the protocol fee amount.max_cancellation_premium
(an absolute lamport amount). When a resolver cancels an expired order using cancel_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).The whitelist
program serves as an access control layer for specific actions within the fusion_swap
program.
taker
(calling fill
) and to cancel expired orders (calling cancel_by_resolver
) to only authorized addresses. This fulfills the "network of professional market makers" concept.WhitelistState
account (a PDA seeded by WHITELIST_STATE_SEED
) which stores the owner
public key.owner
has the authority to manage the whitelist.register
, providing the user's public key. This creates an empty ResolverAccess
account (a PDA seeded by RESOLVER_ACCESS_SEED
and the user's key). The existence of this account signifies that the user is whitelisted.deregister
, which closes the user's ResolverAccess
account.fill
and cancel_by_resolver
instructions in fusion_swap
include constraints that verify that the transaction signer (taker
or resolver
) has a valid ResolverAccess
account derived using the whitelist
program's ID and the correct seeds.Users and participants in this protocol operate under several security assumptions and known risks:
whitelist
program's owner to:
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.
Several roles possess special capabilities within the system:
Whitelist Owner:
whitelist
program's WhitelistState
account.register
)deregister
)transfer_ownership
)Resolvers / Takers:
register
function, resulting in the creation of a corresponding ResolverAccess
PDA.fusion_swap
):
fill
)cancel_by_resolver
)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.
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:
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.
Key parts of the fusion_swap
program lack essential docstrings, reducing clarity and increasing the risk of misuse:
#[program]
): Missing top-level docstring explaining the contract's purpose and functionality.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.order_hash
, get_fee_amounts
, and uni_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.
transfer_ownership
Performs Immediate Ownership Transfer Without SafeguardsThe 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.
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.
_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.
.key()
Call on a Pubkey
ValueIn 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.
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.
toolchain
Section in Anchor.toml
Is EmptyThe 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.
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.
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 in fusion-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 on auction.rs
). This statement is factually incorrect. timestamp
is a u64
(64 bits), and rate_bump
, though originally a u16
value, is cast to u64
for arithmetic purposes. Even considering the original type, the combined bit size is 64 + 16 = 80
, which is not less than 64. The intention appears to be to justify that the time_difference * rate_bump
multiplication cannot overflow a u64
container. In practice, the values are constrained:
u16
(maximum 65535) value.rate_bump
values also originate from a u16
value.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.
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.