1inch Calldata Corruption
Total Losses
$5.0M+
Date
Network
Categories
business logic data validation arithmeticStep-by-step
-
Attacker deploys exploit contract that implements EIP-1271 signature validation and exposes a
settle()function that forwards crafted orders toSettlement.settleOrders(). -
Attacker crafts malicious order payload containing 6 nested orders calculated ABI encoding offsets and a negative interaction length value.
-
Attacker calls
Settlement.settleOrders()which processes the nested order chain recursively through_settleOrder(). -
Integer underflow corrupts suffix write location when the vulnerable Yul assembly calculates where to write the order suffix, the negative length causes the write position to underflow, writing the legitimate suffix over zero-padding instead of at the end of calldata.
-
Fake suffix with victim as resolver is read by the
DynamicSuffixlibrary, which reads from the end of calldata where the attacker placed a crafted suffix containing the victim’s address as the “resolver”. -
Settlement calls
victim.resolveOrders()believing it’s the legitimate resolver. Since the caller is the trusted Settlement contract, the victim (TrustedVolumes) transfers funds to the attacker. -
Attacker receives ~$5M across multiple tokens (USDC, WETH) from TrustedVolumes resolver contract.
Detailed Description
The 1inch Settlement exploit targeted the deprecated Fusion V1 Settlement contract, exploiting a vulnerability in the _settleOrder() function. The attack represents a calldata corruption technique.
Background: 1inch Fusion Architecture
1inch Fusion is a gasless swap protocol where resolvers (market makers) fill user orders. The Settlement contract coordinates order execution by:
- Processing orders through
settleOrders()→_settleOrder() - Appending a “suffix” containing resolver information to each order
- Calling
resolveOrders()on the resolver to execute token transfers
The assumption: only the legitimate resolver should be called, and it trusts the Settlement contract as the caller.
The Vulnerable Code
The vulnerability exists in _settleOrder()’s Yul assembly block:
function _settleOrder(
bytes calldata data,
address resolver,
uint256 totalFee,
bytes memory tokensAndAmounts
) private {
// ... order validation ...
assembly {
// [1] Read interaction offset from attacker-controlled calldata
let interactionLengthOffset := calldataload(add(data.offset, 0x40))
let interactionOffset := add(interactionLengthOffset, 0x20)
// [2] Read interaction length
let interactionLength := calldataload(add(data.offset, interactionLengthOffset))
// ... copy calldata to memory at 'ptr' ...
// [3] Calculate suffix write position - Underflow
let offset := add(add(ptr, interactionOffset), interactionLength)
// [4] Write suffix data including resolver address
mstore(add(offset, 0x04), totalFee)
mstore(add(offset, 0x24), resolver) // <-- Resolver written here
mstore(add(offset, 0x44), takerAsset)
// ... more suffix fields ...
}
}
The code trusts interactionLength from calldata without validation. By providing a massive value (0xFFFF...FE00 = -512 in two’s complement), the offset calculation underflows, causing the suffix to be written in memory.
Attack Mechanics
Phase 1: Crafting the Malicious Payload
The attacker constructs a nested order chain with 6 orders. The malicious calldata uses standard ABI-encoded pointers: order offset at 0xE0, signature offset at 0x240, and interaction offset at 0x460.
After the 320-byte order struct, the attacker places 544 bytes of zero-padding (0x260-0x460), then at 0x460 places -512 as uint256 instead of a legitimate interaction length. At 0x480, the fake suffix contains the victim’s address as resolver.
Phase 2: The Underflow Calculation
When _settleOrder() processes this data:
Normal calculation:
offset = ptr + interactionOffset + interactionLength
= ptr + 0x480 + positive_value
= writes at END of calldata ✓
Attack calculation:
offset = ptr + interactionOffset + (-512)
= ptr + 0x480 - 0x200
= ptr + 0x280
= writes EARLIER, over zero padding ✗
The legitimate suffix (with the correct resolver = attacker’s contract) gets written to the zero-padding region and is ignored. Meanwhile, the DynamicSuffix library reads from the end of calldata, where the attacker placed their fake suffix.
Phase 3: Resolver Hijacking
The fake suffix structure:
bytes memory victimSuffix = abi.encode(
uint256(0), // totalFee (padding)
RESOLVER, // resolver ← THE VICTIM ADDRESS!
address(USDC), // takerAsset
uint256(0), // rateBump (padding)
uint256(0), // takingFee (padding)
address(USDC), // token
DRAIN_AMOUNT, // amount to steal
uint256(0x40) // tokensAndAmounts length
);
When Settlement processes the finalize interaction:
// Settlement thinks resolver = VICTIM (from fake suffix)
// Calls: VICTIM.resolveOrders(VICTIM, tokensAndAmounts, data)
// VICTIM sees msg.sender = Settlement (trusted!) → transfers funds
Why 6 Nested Orders?
Each nested order adds entries to the internal tokensAndAmounts array. The attacker needed sufficient entries so that:
suffixSize = staticFields (160 bytes) + tokensAndAmounts (~320 bytes) + length (32 bytes)
≈ 512 bytes (0x200)
This makes the -512 offset calculation precisely rewind the write position over the zero-padding.
Interaction Flags
The Settlement contract uses single-byte flags to control flow:
// From fillOrderInteraction()
if (interaction[20] == 0x00) {
// CONTINUE: Process next nested order
_settleOrder(interaction[21:], ...);
} else if (interaction[20] == 0x01) {
// FINALIZE: Call resolver.resolveOrders()
target.resolveOrders(resolver, tokensAndAmounts, data);
}
Orders 1-5 use 0x00 (continue) to build the chain. Order 6 uses 0x01 (finalize) to trigger the drain.
Conclusions
The 1inch Settlement exploit represents a rare class of smart contract vulnerability memory corruption in Yul assembly. Several factors contributed to its success:
-
Deprecated but live code: Fusion V1 was deprecated in mid-2023 but remained deployed for backward compatibility, receiving no security updates.
-
Complex interaction patterns: The nested order mechanism, created attack surface that single-order analysis couldn’t reveal.
-
Trusted caller assumption: The victim resolver trusted Settlement as the caller without validating the resolver parameter in the suffix.
The attack was executed across 10 transactions, draining approximately $5M total from the TrustedVolumes resolver. Our PoC reproduces one representative transaction stealing 1M USDC. After on-chain negotiation, most funds were returned, with the attacker keeping a fractional bounty.
Possible Mitigations
1. Input Validation in Assembly
Validate that interactionLength does not exceed data.length before using it in offset calculations.
2. Safe Arithmetic for Offset Calculations
Implement overflow checks on pointer arithmetic in Yul assembly to prevent underflow from negative values.
3. Resolver Self-Validation
Resolver contracts should verify the resolver parameter matches their own address, not just trust the Settlement caller.