Skip to content

feat: USDFree interfaces#1283

Open
grasphoper wants to merge 23 commits intomasterfrom
usdfreev2
Open

feat: USDFree interfaces#1283
grasphoper wants to merge 23 commits intomasterfrom
usdfreev2

Conversation

@grasphoper
Copy link
Contributor

@grasphoper grasphoper commented Jan 30, 2026

Project overview

Why:

Improve sponsored system: the current iteration of the sponsored bridging system is limited to a specific use case: sponsored stable bridging into HyperLiquid using mint/burn bridges.

Unify Across product: mint/burn, Across, sponsored Phase0 are currently different parts of the stack that could benefit from unification in terms of both operational and UX sides.

Goals:

bridge tokens to other chain in a mechanism-agnostic way: CCTP, OFT, Across etc.

use a single upgradeable entry point for order submission so that the users don't ever have to re-approve and integrators don't have to maintain complicated mappings

support gasless

support best-path swapping on either source or destination or both

support user action after the desired amount of tokens is received on destination (like AAVE deposits or deposits to Hyperliquid Core etc.)

support sponsoring transactions to cover the bridge and / or swap fee partially or in full

support operation chaining: allow performing multiple cross-chain actions in a row

mitigate abuse of destination-side actions (e.g. by using obfuscation)

Outcomes: 

Architecture for contracts, API and relayer, showing how the system would function together

Implementation of the above system

Closes ACP-10

@grasphoper grasphoper marked this pull request as ready for review February 4, 2026 03:22
@linear
Copy link

linear bot commented Feb 4, 2026

- token is used to inform the final user action. The action is made with (tokenReq.token, balanceOf(tokenReq.token, address(this))) on the executor
*/
bytes tokenReq; // (token, amount)
// NOTE: if an order is sponsored, this is forced by the API to be one of the trusted relayers, to prevent self-
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does the API enforce this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sponsored orders, API is fully responsible for keeping it's own database of orderId => sponsorshipAmount, so when submitterReq is not one of the trusted entities, the API just doesn't put the orderId into the DB


// The struct that the user signs/submits. Contains all of the user's preferences
struct Order {
// NOTE: there's no (token, amount) here that the user submits. That's provided separately at the time of the submit.. call
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your comment is incomplete here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is complete. submit.. here just means OrderGateway.submit or OrderGateway.submitWithAuction

When submission is direct by the user, it's an approval-based flow and relies on approvals and (token, amount) decoded from funding argument.

If submission is gasless, funding still holds the relevant information, e.g. in IPermit2.PermitTransferFrom:

    struct TokenPermissions {
        address token;
        uint256 amount;
    }

    struct PermitTransferFrom {
        TokenPermissions permitted;
        uint256 nonce;
        uint256 deadline;
    }

- amount == 0 means no enforccement
- token is used to inform the final user action. The action is made with (tokenReq.token, balanceOf(tokenReq.token, address(this))) on the executor
*/
bytes tokenReq; // (token, amount)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the thinking here that this is type bytes if this is always going to be abi.encode(address, uint256)? I guess i'm just wondering why not just explicitly require amount and token instead of squashing them into bytes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make this extensible without changing the format of the ExecutionStep. I think the idea is more so make it

(tokenReqTypeEnum, someOtherData)

Which, to start can be something like (0, (token, amount))

But also could be something like an onchain dutch auction: (1, (startingPrice, startingTime, endPrice))

This is interpreted and checked by the internal function on the Executor contract, so as we expand it's functionality, we can add more types to the token requirement

// A struct that represents some change in user requirement as a result of the auction
struct Change {
// enum for: token, deadline, submitter or custom
uint8 typ;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type?

Suggested change
uint8 typ;
uint8 type;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't use type, it's a reserved keyword

struct Change {
// enum for: token, deadline, submitter or custom
uint8 typ;
// NOTE: if a type is custom, data can inlcude an index of the otherStaticReqs that the auction wants to change. The
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// NOTE: if a type is custom, data can inlcude an index of the otherStaticReqs that the auction wants to change. The
// NOTE: if a type is custom, data can include an index of the otherStaticReqs that the auction wants to change. The

// enum for: token, deadline, submitter or custom
uint8 typ;
// NOTE: if a type is custom, data can inlcude an index of the otherStaticReqs that the auction wants to change. The
// funtion to apply the change has to be implemented on the OrderGateway for this to work
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// funtion to apply the change has to be implemented on the OrderGateway for this to work
// function to apply the change has to be implemented on the OrderGateway for this to work

// NOTE: only approval-based token pulls are supported from submitters
TokenAmount[] extraFunding;
bytes actions; // MulticallHandler.Instructions or weiroll (support can be expanded to any format)
bytes deobfuscation; // if user's finalAction is obfuscated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how would deobfuscation bytes work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OrderStore handles the deobfuscation. If length hashOrUserAction is 32 bytes, safe to assume it's a hash. Or we could just add a single byte at the start to specify whether this is obfuscated to start with.

Then the OrderStore receives deobfuscated data as a part of submitter data, checks against the hash and propagates to Executor. So the Executor never deals with deobfusaction

function execute(
// `ExecutionStep.tokenReq.token`
address token,
// `balanceOf(address(IExecutor), ExecutionStep.tokenReq.token)`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't the balanceOf step vulnerable to someone dropping tokens on to the contract?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a problem? Executor contract is not at all suitable for escrowing any funds (it allows submitter to do anything with the funds on the contract as long as the tokenReq is met). Seems fine

- Checks requirements after submitter actions complete
- Calls IUserActionExecutor with token, amount, params, and remaining steps

**IUserActionExecutor** - Final action interface
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this called an I-contract, and what's the reasoning behind the naming? The executor/userActionExecutor names aren't the most intuitive to me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I-contract to show that it's an interface and there are many implementations possible, in contrast to something like an Executor, which is a single contract.

As for name intuitiveness, these are open to change, open to suggestions!

2. **OrderGateway** applies auction changes if present (signed by auctionAuthority)
3. **Executor** runs submitter actions
4. **Executor** checks all requirements are met (token balance, submitter, deadline, etc.)
5. **Executor** calls **IUserActionExecutor** with current step's action
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you keep mentioning in the docs that the Executor executes a single step. This implies that an action might have multiple steps. How would the execution flow look like for a multi step action? And what would a multi step action even do, in practice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single step is executed on one chain. A step is:

  • submitter actions
  • check user reqs
  • user action

What could 2 steps do?

For example, mint-burn + deposit into Hyperliquid:

[srcStep, dstStep]

  • srcStep: tokenReq == input amount (so no submitter actions needed). user action = deposit into CCTP
  • dstStep: check tokenReq (e.g. input from source - a few bps) + call HyperliquidDepositHandler.depositToHypercore

Alternatively, a single-step similar thing could be:

[srcStep]

tokenReq == input amount, user action == deposit into spokepool such that message = HyperliquidDepositHandler interaction

In the case where there are 2 actions, mint-burn dst contracts are interacting with OrderStore. OrderStore on dst is analogous to OrderGateway on src, expect it doesn't apply auction changes and doesn't calculate orderId

Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Signed-off-by: Ihor Farion <ihor@umaproject.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants