Skip to content
Closed
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
4 changes: 2 additions & 2 deletions config/clearSigningProposal.json
Original file line number Diff line number Diff line change
Expand Up @@ -1572,7 +1572,7 @@
}
]
},
"startBridgeTokensViaMayan((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (bytes32 nonEVMReceiver, address mayanProtocol, bytes protocolData, address swapProtocol, bytes swapData, address middleToken, uint256 minMiddleAmount, address refundRecipient) _mayanData)": {
"startBridgeTokensViaMayan((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (bytes32 nonEVMReceiver, address mayanProtocol, bytes protocolData, address swapProtocol, bytes swapData, address middleToken, uint256 minMiddleAmount, address refundRecipient, bytes signature, uint256 deadline) _mayanData)": {
"intent": "Bridge via Mayan",
"interpolatedIntent": "Bridge {_bridgeData.minAmount} via Mayan to chain {_bridgeData.destinationChainId} for {_bridgeData.receiver}",
"fields": [
Expand Down Expand Up @@ -4756,7 +4756,7 @@
}
]
},
"swapAndStartBridgeTokensViaMayan((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (address callTo, address approveTo, address sendingAssetId, address receivingAssetId, uint256 fromAmount, bytes callData, bool requiresDeposit)[] _swapData, (bytes32 nonEVMReceiver, address mayanProtocol, bytes protocolData, address swapProtocol, bytes swapData, address middleToken, uint256 minMiddleAmount, address refundRecipient) _mayanData)": {
"swapAndStartBridgeTokensViaMayan((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (address callTo, address approveTo, address sendingAssetId, address receivingAssetId, uint256 fromAmount, bytes callData, bool requiresDeposit)[] _swapData, (bytes32 nonEVMReceiver, address mayanProtocol, bytes protocolData, address swapProtocol, bytes swapData, address middleToken, uint256 minMiddleAmount, address refundRecipient, bytes signature, uint256 deadline) _mayanData)": {
"intent": "Swap & Bridge via Mayan",
"interpolatedIntent": "Swap then bridge {_bridgeData.minAmount} via Mayan to chain {_bridgeData.destinationChainId} for {_bridgeData.receiver}",
"fields": [
Expand Down
6 changes: 6 additions & 0 deletions config/mayan.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{
"staging": {
"backendSigner": "0x981CCF8c09633F6F2AF3fe661C285ca1DB09caE1"
},
"production": {
"backendSigner": "0xAF4B7A83591a6c4c8B9d1341C3F08BBc3b800fc5"
},
"bridges": {
"mainnet": {
"bridge": "0x337685fdaB40D39bd02028545a4FfA7D287cC3E2"
Expand Down
38 changes: 35 additions & 3 deletions docs/MayanFacet.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,38 @@ graph LR;

## Receiver validation

Before forwarding to Mayan, the facet validates that the order's destination receiver matches `BridgeData.receiver`:
The facet has two trust models, partitioned strictly by `BridgeData.hasDestinationCall`:

- **Plain bridges (`hasDestinationCall == false`)** — permissionless, unchanged: before forwarding to Mayan, the facet validates that the order's destination receiver matches `BridgeData.receiver`:
- **EVM destinations:** the receiver parsed from the Mayan protocol data (`destAddr`) must equal `BridgeData.receiver`.
- **Non-EVM destinations** (`BridgeData.receiver == NON_EVM_ADDRESS`): the receiver parsed from the protocol data must equal `MayanData.nonEVMReceiver` (full 32-byte comparison).
- **Destination calls (`hasDestinationCall == true`)** — signature-gated (see [Arbitrary destination calls](#arbitrary-destination-calls)): the on-chain `_parseReceiver` check is **skipped** and the receiver is instead attested by a backend EIP-712 signature.

## Arbitrary destination calls

A Mayan Swift v2 order can carry a `customPayload` that instructs Mayan's destination-side executor to perform an **arbitrary call** (target + calldata) after delivery. That target/calldata **cannot be validated on-chain** against `BridgeData.receiver` — the whole point of a destination call is that funds/control flow go somewhere other than a plain receiver.

Following the repo's opaque-calldata convention, this capability is unlocked **only** behind a backend EIP-712 signature. When `BridgeData.hasDestinationCall == true`, both entrypoints verify — before any deposit or swap, over the **as-submitted** `protocolData` and **pre-swap** `minAmount` — a signature from the authorized backend signer over:

```text
MayanDestinationCallPayload(
bytes32 transactionId,
bytes32 receiver, // NON_EVM-aware: nonEVMReceiver when receiver == NON_EVM_ADDRESS
uint256 minAmount, // pre-swap minAmount, as submitted
address sendingAssetId,
uint256 destinationChainId,
bool hasDestinationCall,
address mayanProtocol,
bytes32 protocolDataHash, // keccak256(MayanData.protocolData), which contains the customPayload
uint256 deadline
)
```

The domain separator binds `block.chainid` and `address(this)` (the diamond, under delegatecall), so a signature is valid on exactly one chain and one diamond. `deadline` time-boxes every signature, and `BridgeData.transactionId` is consumed in diamond storage (CEI, before the external Mayan call) to prevent same-chain replay.

### Trust assumption (destination calls)

- **EVM destinations:** the receiver parsed from the Mayan protocol data (`destAddr`) must equal `BridgeData.receiver`.
- **Non-EVM destinations** (`BridgeData.receiver == NON_EVM_ADDRESS`): the receiver parsed from the protocol data must equal `MayanData.nonEVMReceiver` (full 32-byte comparison).
On the destination-call path the receiver is **signer-attested, not on-chain-enforced**: the facet does not (and cannot) validate the opaque call target. The backend is trusted to only sign payloads it constructed for legitimate routes. A leaked signer key can authorize an arbitrary destination call, but the blast radius is bounded to the funds of the specific signed transaction — the facet custodies nothing persistently. The signer is **immutable**; rotation is a governance action (deploy a new facet with the new signer and run `diamondCut` through the Safe/timelock). There is deliberately **no owner setter** for it.

### HyperCore deposits

Expand Down Expand Up @@ -62,6 +90,8 @@ The methods listed above take a variable labeled `_mayanData`. This data is spec
/// @param minMiddleAmount The minimum middleToken amount that must result from the swap
/// @param refundRecipient The address that receives excess native value and swap leftovers;
/// must be the user, never the relayer, so refunds are not stranded on a relayer
/// @param signature The backend EIP-712 signature; consumed only when hasDestinationCall
/// @param deadline The signature expiry; enforced only on the signed destination-call path
struct MayanData {
bytes32 nonEVMReceiver;
address mayanProtocol;
Expand All @@ -71,6 +101,8 @@ struct MayanData {
address middleToken;
uint256 minMiddleAmount;
address refundRecipient;
bytes signature;
uint256 deadline;
}
```

Expand Down
3 changes: 3 additions & 0 deletions script/demoScripts/demoMayan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ const main = async () => {
middleToken: '0x0000000000000000000000000000000000000000',
minMiddleAmount: 0,
refundRecipient: address,
// Destination-call signature fields; unused on this plain (non-dest-call) path
signature: '0x',
deadline: 0,
}

console.info('Dev Wallet Address: ', address)
Expand Down
15 changes: 14 additions & 1 deletion script/deploy/facets/DeployMayanFacet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ contract DeployScript is DeployScriptBase {
string.concat(".bridges.", network, ".bridge")
);

return abi.encode(bridge);
string memory json = vm.readFile(path);

// check if production or staging
address backendSigner;
if (
keccak256(abi.encodePacked(fileSuffix)) ==
keccak256(abi.encodePacked("staging."))
) {
backendSigner = json.readAddress(".staging.backendSigner");
} else {
backendSigner = json.readAddress(".production.backendSigner");
}

return abi.encode(bridge, backendSigner);
}
}
14 changes: 14 additions & 0 deletions script/deploy/resources/deployRequirements.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,20 @@
}
}
},
"MayanFacet": {
"configData": {
"_mayan": {
"configFileName": "mayan.json",
"keyInConfigFile": ".bridges.<NETWORK>.bridge",
"allowToDeployWithZeroAddress": "false"
},
"_backendSigner": {
"configFileName": "mayan.json",
"keyInConfigFile": ".<ENVIRONMENT>.backendSigner",
"allowToDeployWithZeroAddress": "false"
}
}
},
"NEARIntentsFacet": {
"configData": {
"_backendSigner": {
Expand Down
Loading
Loading