Skip to content
Draft
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
25 changes: 13 additions & 12 deletions crates/tycho-ethereum/src/services/token_analyzer/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use alloy::sol;

/// Runtime bytecode for `contracts/Analyzer.sol`.
pub const ANALYZER_BYTECODE: &[u8] = &alloy::primitives::hex!(
"6080604052600436101561001257600080fd5b6000803560e01c63521c65391461002857600080fd5b346100bf5760803660031901126100bf576001600160a01b039060043582811681036100c2576044359183831683036100bf5760643593841684036100bf5761014061007885856024358661013e565b976040979197969296959395519915158a52151560208a0152151560408901526060880152608087015260a086015260c085015260e0840152610100830152610120820152f35b80fd5b5080fd5b90601f8019910116810190811067ffffffffffffffff8211176100e857604052565b634e487b7160e01b600052604160045260246000fd5b90816020910312610116575180151581036101165790565b600080fd5b9190820391821161012857565b634e487b7160e01b600052601160045260246000fd5b92939160018060a01b03948585169460408051946370a0823160e01b9384875289600496169889878901526020602491818a8481845afa998a156105cd5760009a61059e575b50898287519e8f968b88521695868c8201528581855afa9081156104dc578d9e60009261056c575b5081985a9e895198868a019163a9059cbb60e01b8352888b015260448a015260448952608089019867ffffffffffffffff998181108b821117610557578b525160009283929083905af1963d1561054f573d90811161053b5788519061021b601f8201601f19168701836100c6565b81523d60008683013e5b87610508575b50610238879e5a9061011b565b96156104e7575050855198888a528d818b0152828a8581855afa998a156104dc5760009a6104ad575b50898b811061049157506102bf9c9d61027a8c8c61011b565b955a96895190631339c64960e01b825285858301528288830152604482015285816064816000875af160009181610472575b5061046a575060009e8f975b5a9061011b565b96156103ee5788518b8152828482015285818881885afa9081156103b6576000916103c1575b509a8951908152818482015285818881885afa9081156103b65790869594939291600091610380575b5091600091606494939b5198899687956361be88e160e11b8752860152840152811960448401525af1918291600093610351575b505061034e5750600097565b97565b610371929350803d10610379575b61036981836100c6565b8101906100fe565b903880610342565b503d61035f565b86819395949792503d83116103af575b61039a81836100c6565b8101031261011657518594919290600061030e565b503d610390565b8a513d6000823e3d90fd5b90508581813d83116103e7575b6103d881836100c6565b810103126101165751386102e5565b503d6103ce565b509683969e50809a9b9c989992919493959d5051968794859384528301525afa9485156104605750600094610430575b50600197600097889796959493889350565b9080929450813d8311610459575b61044881836100c6565b81010312610116575191388061041e565b503d61043e565b513d6000823e3d90fd5b9e8f976102b8565b61048a919250873d89116103795761036981836100c6565b90386102ac565b60009e508e9d508d9c909a508c99508997508795509350505050565b90998382813d83116104d5575b6104c481836100c6565b810103126100bf5750519838610261565b503d6104ba565b87513d6000823e3d90fd5b60009e508e9d508d9c909b508c9a508a995090975088965086945092505050565b809197505190848215928315610523575b505050953861022b565b61053393508201810191016100fe565b388481610519565b8560418d634e487b7160e01b600052526000fd5b506060610225565b508760418f634e487b7160e01b600052526000fd5b9d509050828d813d8111610597575b61058581836100c6565b81010312610116578d9c5190386101ac565b503d61057b565b9099508181813d83116105c6575b6105b681836100c6565b8101031261011657519838610184565b503d6105ac565b86513d6000823e3d90fd"
"610100604052600436101561001357600080fd5b6000803560e01c63521c65391461002957600080fd5b346100fc5760803660031901126100fc57600435906001600160a01b039081831683036100fc576044359082821682036100fc5760643592831683036100fc5750602435610076936101b7565b9b909b9a919a9992999893989794979695969b9a999897969594939291906080519d906040519e8f92151583521515602083015215159060400152151560608d015260808c015260a08b015260c08a015260e08901526101008801526101208701526101408601526101608501526101808401526101a08301526101c08201526101e090f35b80fd5b90601f8019910116810190811067ffffffffffffffff82111761012157604052565b634e487b7160e01b600052604160045260246000fd5b3d15610172573d9067ffffffffffffffff82116101215760405191610166601f8201601f1916602001846100ff565b82523d6000602084013e565b606090565b9081602091031261018f5751801515810361018f5790565b600080fd5b919082039182116101a157565b634e487b7160e01b600052601160045260246000fd5b60e05260c05260a052600060805260018060a01b0391604051926370a0823160e01b84528060a0511660048501526020846024818460e051165afa9384156105ff576000946108c0575b508390604051936370a0823160e01b85523060048601526020856024818560e051165afa9485156105ff5760009561088c575b50846040516370a0823160e01b815283831660048201526020816024818760e051165afa9081156105ff5760009161085a575b5080965a60405163a9059cbb60e01b6020820190815260c05160a0516001600160a01b031660248401526044830152919a916000918291906102b681606481015b03601f1981018352826100ff565b51908260e0515af1926102c7610137565b84610828575b506102da849b5a90610194565b931561080057505050604051936370a0823160e01b85528060a0511660048601526020856024818460e051165afa9485156105ff576000956107cc575b50848681106107a9575061039461032e8787610194565b61036d60205a604051631339c64960e01b815260e0516001600160a01b0316600482015230602482015260448101949094529291829081906064820190565b038160008860a051165af160009181610788575b50610780575060006080525b5a90610194565b97604051946370a0823160e01b86523060048701526020866024818660e051165afa9586156105ff5760009661074c575b5085936040516370a0823160e01b81528460a0511660048201526020816024818860e051165afa9081156105ff5760009161071a575b508097608051156106fb57505060c051850185116101a1576104218660c0518701610194565b60405163a9059cbb60e01b6020820190815260a0516001600160a01b0316602483015260448201839052919c9160009182919061046181606481016102a8565b51908260e0515af150610472610137565b506104b560205a604051631339c64960e01b815260e0516001600160a01b03908116600483015287166024820152604481019f909f529d91829081906064820190565b038160008a60a051165af1600091816106da575b506106d057506104dc60009c5a90610194565b8c1561063d576040516370a0823160e01b81528660a0511660048201526020816024818a60e051165afa9081156105ff5760009161060b575b50956040516370a0823160e01b815281861660048201526020816024818560e051165afa9081156105ff5782906000926105ca575b50606460209297600060405195869485936361be88e160e11b85528160e0511660048601521660248401528119604484015260a051165af160009181610599575b50610596575060009b565b9b565b6105bc91925060203d6020116105c3575b6105b481836100ff565b810190610177565b903861058b565b503d6105aa565b9150506020813d6020116105f7575b816105e6602093836100ff565b8101031261018f575181606461054a565b3d91506105d9565b6040513d6000823e3d90fd5b90506020813d602011610635575b81610626602093836100ff565b8101031261018f575138610515565b3d9150610619565b9b509b509050602060249495969798999293604051958680926370a0823160e01b82528060a05116600483015260e051165afa9384156105ff5760009461069c575b5060019a8b60805260009a60009a99989796959493600093929190565b9093506020813d6020116106c8575b816106b8602093836100ff565b8101031261018f5751923861067f565b3d91506106ab565b6104dc909c61038d565b6106f491925060203d6020116105c3576105b481836100ff565b90386104c9565b6000608081905260019e509c8d9c9299509096508b9550859350908390565b90506020813d602011610744575b81610735602093836100ff565b8101031261018f5751386103fb565b3d9150610728565b9095506020813d602011610778575b81610768602093836100ff565b8101031261018f575194386103c5565b3d915061075b565b60805261038d565b6107a291925060203d6020116105c3576105b481836100ff565b9038610381565b600060808190529950899889989196508895508593849350909183919082908190565b90946020823d6020116107f8575b816107e7602093836100ff565b810103126100fc5750519338610317565b3d91506107da565b600060808190529a508a99508998919750889650869550919350849283929183919082908190565b80519194508115918215610840575b505092386102cd565b6108539250602080918301019101610177565b3880610837565b90506020813d602011610884575b81610875602093836100ff565b8101031261018f575138610267565b3d9150610868565b9094506020813d6020116108b8575b816108a8602093836100ff565b8101031261018f57519338610234565b3d91506108b8565b9093506020813d6020116108ec575b816108dc602093836100ff565b8101031261018f57519238610201565b3d91506108cf56"
);

/// Runtime bytecode for `contracts/Forwarder.sol`.
Expand All @@ -42,14 +42,19 @@ sol! {
address recipient
) external returns (
bool transferInOk,
bool roundtripOutOk,
bool transferOutOk,
bool approvalOk,
uint256 balanceBeforeIn,
uint256 balanceAfterIn,
uint256 balanceAfterRoundtrip,
uint256 holderBefore,
uint256 holderAfter,
uint256 balanceAfterOut,
uint256 recipientBefore,
uint256 recipientAfter,
uint256 gasIn,
uint256 roundtripGasOut,
uint256 gasOut
);
}
Expand All @@ -62,20 +67,16 @@ mod tests {

#[test]
fn bytecodes_are_valid_evm() {
// EVM contracts compiled via --via-ir start with PUSH1 0x80 (0x60 0x80).
// Contracts compiled via --via-ir start with a PUSH opcode (0x60 = PUSH1, 0x61 = PUSH2)
// for the free memory pointer initialisation. Larger contracts use PUSH2.
fn is_valid_evm_header(code: &[u8]) -> bool {
matches!(code.first(), Some(0x60 | 0x61))
}
assert!(ANALYZER_BYTECODE.len() > 10, "analyzer bytecode is suspiciously short");
assert_eq!(
&ANALYZER_BYTECODE[..2],
&[0x60, 0x80],
"analyzer bytecode has wrong EVM header"
);
assert!(is_valid_evm_header(ANALYZER_BYTECODE), "analyzer bytecode has wrong EVM header");

assert!(FORWARDER_BYTECODE.len() > 10, "forwarder bytecode is suspiciously short");
assert_eq!(
&FORWARDER_BYTECODE[..2],
&[0x60, 0x80],
"forwarder bytecode has wrong EVM header"
);
assert!(is_valid_evm_header(FORWARDER_BYTECODE), "forwarder bytecode has wrong EVM header");
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,70 @@ interface IForwarder {
}

/// @title Token Analyzer
/// @notice Injected at a token holder's address via eth_call state override. Simulates a full
/// round-trip ERC20 transfer (holder -> settlement -> recipient) in a single call and reports
/// balances, gas costs, and success flags. No on-chain deployment required.
/// @dev The inbound transfer uses a low-level call rather than a typed interface so that tokens with
/// non-standard transfer() implementations (e.g. USDT, which omits the bool return value) are
/// handled correctly. balanceOf and approve are called via the typed interface since they are
/// consistently implemented across tokens.
/// @notice Injected at a token holder's address via eth_call state override. Performs a full
/// sell-fee analysis in a single call using four transfer legs:
///
/// Leg 1 (buy): holder → settlement — detects buy fees
/// Leg 2 (roundtrip): settlement → holder — detects fees even to whitelisted LP
/// Leg 3 (retransfer): holder → settlement — returns tokens for the sell test
/// Leg 4 (sell): settlement → recipient — detects sell-only fees
///
/// If the roundtrip (legs 1-2) passes but the sell test (legs 3-4) fails, the token has a
/// sell-only fee: it charges fees only when transferring to non-whitelisted addresses.
///
/// @dev Inbound transfers (legs 1 and 3) use low-level calls so that tokens with non-standard
/// transfer() implementations (e.g. USDT, which omits the bool return value) are handled
/// correctly. balanceOf and approve are called via the typed interface.
contract Analyzer {
/// @notice Simulate ERC20 transfer in and out, measuring balances and gas at each step.
/// @param token The ERC20 token to analyze.
/// @param amount The amount to transfer in from this address (the holder).
/// @param settlement Intermediary address (injected with Forwarder bytecode).
/// @param recipient Final recipient of the outbound transfer.
/// @return transferInOk Whether transfer(settlement, amount) succeeded.
/// @return transferOutOk Whether forwardTransfer(token, recipient, received) succeeded.
/// @return approvalOk Whether forwardApprove(token, recipient, MAX_UINT256) succeeded.
/// @return balanceBeforeIn Settlement balance before transfer in.
/// @return balanceAfterIn Settlement balance after transfer in.
/// @return balanceAfterOut Settlement balance after transfer out.
/// @return recipientBefore Recipient balance before any transfer.
/// @return recipientAfter Recipient balance after transfer out.
/// @return gasIn Gas consumed by the inbound transfer (gasleft() delta).
/// @return gasOut Gas consumed by the outbound transfer (gasleft() delta).
/// @notice Simulate a full ERC20 transfer analysis in four legs.
/// @param token The ERC20 token to analyze.
/// @param amount The amount to transfer in each buy leg.
/// @param settlement Intermediary address (injected with Forwarder bytecode).
/// @param recipient Final arbitrary recipient for the sell-only fee test.
/// @return transferInOk Whether leg 1 (holder → settlement) succeeded.
/// @return roundtripOutOk Whether leg 2 (settlement → holder) succeeded.
/// @return transferOutOk Whether leg 4 (settlement → recipient) succeeded.
/// @return approvalOk Whether forwardApprove(MAX_UINT256) succeeded.
/// @return balanceBeforeIn Settlement balance before leg 1.
/// @return balanceAfterIn Settlement balance after leg 1.
/// @return balanceAfterRoundtrip Settlement balance after leg 2.
/// @return holderBefore Holder balance before leg 1.
/// @return holderAfter Holder balance after leg 2.
/// @return balanceAfterOut Settlement balance after leg 4.
/// @return recipientBefore Recipient balance before leg 4.
/// @return recipientAfter Recipient balance after leg 4.
/// @return gasIn Gas consumed by leg 1.
/// @return roundtripGasOut Gas consumed by leg 2.
/// @return gasOut Gas consumed by leg 4.
function analyze(
address token,
uint256 amount,
address settlement,
address recipient
) external returns (
bool transferInOk,
bool roundtripOutOk,
bool transferOutOk,
bool approvalOk,
uint256 balanceBeforeIn,
uint256 balanceAfterIn,
uint256 balanceAfterRoundtrip,
uint256 holderBefore,
uint256 holderAfter,
uint256 balanceAfterOut,
uint256 recipientBefore,
uint256 recipientAfter,
uint256 gasIn,
uint256 roundtripGasOut,
uint256 gasOut
) {
IERC20 erc20 = IERC20(token);

// Read pre-transfer balances for both the settlement and the final recipient.
balanceBeforeIn = erc20.balanceOf(settlement);
holderBefore = erc20.balanceOf(address(this));
recipientBefore = erc20.balanceOf(recipient);

// Transfer from holder (this address) to settlement using a low-level call so that tokens
// which omit the bool return value (e.g. USDT) do not cause a revert during ABI decoding.
// Success condition: call did not revert AND, if return data is present, it decodes true.
// === Leg 1: Buy — holder → settlement ===
uint256 g1 = gasleft();
{
(bool ok, bytes memory data) = token.call(
Expand All @@ -71,41 +86,69 @@ contract Analyzer {
gasIn = g1 - gasleft();

if (!transferInOk) {
return (false, false, false, balanceBeforeIn, 0, 0, recipientBefore, 0, gasIn, 0);
return (false, false, false, false, balanceBeforeIn, 0, 0, holderBefore, 0, 0, recipientBefore, 0, gasIn, 0, 0);
}

balanceAfterIn = erc20.balanceOf(settlement);

// Guard: a token that returns true but reduces the settlement balance is pathological.
// Without this check Solidity 0.8 checked arithmetic would revert the entire eth_call,
// making the result undecodable. Instead we surface it as a transfer failure.
// Guard: a token that returns true but reduces settlement balance is pathological.
if (balanceAfterIn < balanceBeforeIn) {
return (false, false, false, balanceBeforeIn, balanceAfterIn, 0, recipientBefore, 0, gasIn, 0);
return (false, false, false, false, balanceBeforeIn, balanceAfterIn, 0, holderBefore, 0, 0, recipientBefore, 0, gasIn, 0, 0);
}

// received may be less than amount for fee-on-transfer tokens.
uint256 received = balanceAfterIn - balanceBeforeIn;

// Transfer out from settlement to recipient via the injected Forwarder.
// === Leg 2: Roundtrip — settlement → holder ===
uint256 g2 = gasleft();
try IForwarder(settlement).forwardTransfer(token, recipient, received) returns (bool success) {
transferOutOk = success;
try IForwarder(settlement).forwardTransfer(token, address(this), received) returns (bool s) {
roundtripOutOk = s;
} catch {
roundtripOutOk = false;
}
roundtripGasOut = g2 - gasleft();

holderAfter = erc20.balanceOf(address(this));
balanceAfterRoundtrip = erc20.balanceOf(settlement);

if (!roundtripOutOk) {
return (true, false, false, false, balanceBeforeIn, balanceAfterIn, balanceAfterRoundtrip, holderBefore, holderAfter, 0, recipientBefore, 0, gasIn, roundtripGasOut, 0);
}

// === Leg 3: Retransfer — holder → settlement (returns the received tokens for the sell test) ===
// holderAfter = holderBefore - amount + roundtripReceived
// ⟹ roundtripReceived = holderAfter + amount - holderBefore
// Safe: holderAfter >= holderBefore - amount (holder only gains from roundtrip),
// so holderAfter + amount >= holderBefore (no underflow).
uint256 roundtripReceived = holderAfter + amount - holderBefore;
{
(bool ok, bytes memory data) = token.call(
abi.encodeWithSelector(0xa9059cbb, settlement, roundtripReceived)
);
// Intentionally not checked: if this fails, leg 4 will also fail (settlement
// has no tokens), and transferOutOk will correctly capture the outcome.
(ok, data);
}

// === Leg 4: Sell — settlement → recipient (arbitrary) ===
uint256 g3 = gasleft();
try IForwarder(settlement).forwardTransfer(token, recipient, roundtripReceived) returns (bool s) {
transferOutOk = s;
} catch {
transferOutOk = false;
}
gasOut = g2 - gasleft();
gasOut = g3 - gasleft();

if (!transferOutOk) {
balanceAfterOut = erc20.balanceOf(settlement);
return (true, false, false, balanceBeforeIn, balanceAfterIn, balanceAfterOut, recipientBefore, 0, gasIn, gasOut);
return (true, true, false, false, balanceBeforeIn, balanceAfterIn, balanceAfterRoundtrip, holderBefore, holderAfter, balanceAfterOut, recipientBefore, 0, gasIn, roundtripGasOut, gasOut);
}

balanceAfterOut = erc20.balanceOf(settlement);
recipientAfter = erc20.balanceOf(recipient);

// Test that settlement can approve (some tokens block approvals from contracts).
try IForwarder(settlement).forwardApprove(token, recipient, type(uint256).max) returns (bool success) {
approvalOk = success;
try IForwarder(settlement).forwardApprove(token, recipient, type(uint256).max) returns (bool s) {
approvalOk = s;
} catch {
approvalOk = false;
}
Expand Down
Loading
Loading