Bidirectional cross-chain swaps between Ethereum and Sui using 1inch Fusion+ extended protocol
Sway is a production-ready implementation that extends 1inch Fusion+ to enable secure, trustless cross-chain atomic swaps between Ethereum and Sui blockchain. The project bridges the gap between EVM and Move ecosystems by implementing hash-timelock contracts (HTLC) that work seamlessly across both networks.
Key features include bidirectional swap capabilities (EthereumโSui), automated event-driven relayer system, and preservation of atomic guarantees through cryptographic hashlocks. The implementation leverages existing battle-tested 1inch contracts on Ethereum while introducing custom Move smart contracts on Sui, ensuring compatibility with both blockchain architectures.
Users can swap tokens between chains with complete security - either both transactions succeed or both revert, eliminating the risk of partial execution. The system supports any ERC20 tokens on Ethereum and Sui coin types, with gas-optimized execution and real-time cross-chain event propagation.
Built for developers and DeFi protocols seeking reliable cross-chain infrastructure, Sway demonstrates how traditional AMM protocols can be extended to support next-generation blockchains while maintaining security and decentralization principles.
This section explains the technical implementation of extending 1inch Fusion+ to support Sui blockchain.
The implementation bridges two fundamentally different blockchain architectures:
Key Challenge: Maintaining atomic swap guarantees across incompatible hash functions and execution models.
shared_locker.move
- Maker-initiated swaps:
public entry fun maker_lock<T>(
coin: coin::Coin<T>,
hashlock: vector<u8>, // BLAKE2b hash of secret
unlock_date: u64, // Timestamp for refund
resolver: address, // Who can claim with secret
ctx: &mut TxContext,
) {
let locker = SharedLocker {
id: object::new(ctx),
coin,
hashlock,
unlock_date,
maker: ctx.sender(),
resolver,
};
transfer::share_object(locker); // Make accessible to resolver
}
public entry fun claim_shared<T>(
locker: SharedLocker<T>,
secret: vector<u8>, // Reveals the secret
receiver: address,
clock_obj: &clock::Clock,
ctx: &mut TxContext,
) {
assert!(hash::blake2b256(&secret) == locker.hashlock, E_HASH_MISMATCH);
let ev = SrcSecretRevealed { secret };
event::emit<SrcSecretRevealed>(ev); // Triggers cross-chain relay
}
locker.move
- Resolver-initiated swaps:
public entry fun lock<T>(
coin: coin::Coin<T>,
hashlock: vector<u8>,
unlock_date: u64,
receiver: address,
ctx: &mut TxContext,
): Locker<T> {
Locker {
id: object::new(ctx),
coin,
hashlock,
unlock_date,
locker: ctx.sender(),
receiver,
}
}
Problem: Sui uses BLAKE2b, Ethereum uses Keccak256 - same secret produces different hashes.
Solution: Dual hash generation in TypeScript:
// Generate single secret, create both hash types
const secret = crypto.randomBytes(32);
const hashBlake = blake2b(secret, { dkLen: 32 }); // For Sui
const hashKeccak = keccak256(secret); // For Ethereum
// Store mapping for cross-chain verification
const mapping = {
[hashKeccak]: {
lockerId: suiLockerId,
escrowAddress: ethEscrowAddress,
},
};
Core Logic: Listen for secret reveals on both chains and propagate cross-chain.
// Sui Event Listener
await suiProvider.subscribeEvent({
filter: { MoveEventType: `${PACKAGE}::shared_locker::SrcSecretRevealed` },
onMessage: async (event) => {
const secret = new Uint8Array(event.parsedJson.secret);
await submitSecretToEthereum(secret); // Cross-chain relay
},
});
// Ethereum Event Listener
const filter = escrowContract.filters.SecretRevealed();
escrowContract.on(filter, async (secret, escrowAddress) => {
await submitSecretToSui(secret, lockerId); // Cross-chain relay
});
Reused 1inch Contracts: No modifications needed to existing Fusion+ contracts.
// Existing 1inch EscrowFactory creates escrows with Keccak256 hashlocks
contract EscrowSrc {
function withdraw(bytes32 secret) external {
require(keccak256(abi.encodePacked(secret)) == hashlock);
// Transfer tokens and emit SecretRevealed event
}
}
maker_lock()
on Sui with BLAKE2b hashlockclaim_shared()
on Sui, revealing secretSrcSecretRevealed
event, extracts secretwithdraw()
on Ethereum escrow with same secretlock()
on Sui with corresponding BLAKE2b hashlockclaim()
on Sui, revealing secret// Sui: Timestamp-based expiry
assert!(clock::timestamp_ms(clock_obj) < locker.unlock_date, E_CLAIM_TOO_EARLY);
// Ethereum: Block-based expiry
require(block.timestamp < timelock, "Timelock expired");