Skip to content

Function signature collision #242

Description

@0xJackJun

Describe the bug

The data decoder returns incorrect parameter names for proxy contracts whose implementation is verified on Etherscan.

When decoding mint(address,uint256,uint256) on a TransparentUpgradeableProxy whose implementation (ComplianceToken) is verified and proxy-linked on Etherscan, the decoder falls back to the 4-byte signature database instead of using the implementation contract's ABI.

Root cause: Etherscan's getabi API for a proxy address returns the proxy contract's own ABI (TransparentUpgradeableProxy — only constructor/events/errors, no application functions), not the implementation's ABI. The decoder does not follow up by checking getsourcecode for Proxy: 1 + Implementation and fetching the implementation's ABI.

To Reproduce

1. Call the safe-decoder-service:

curl -X POST "https://safe-decoder.safe.global/api/v1/data-decoder" \
  -H "Content-Type: application/json" \
  -d '{
    "data": "0x156e29f60000000000000000000000009ace6120ded418589593c5d953e8f6778ed94b6c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001",
    "to": "0x9ace6120DED418589593C5D953e8F6778Ed94b6c",
    "chain_id": 11155111
  }'

Response (incorrect):

{
  "method": "mint",
  "parameters": [
    { "name": "_to", "type": "address", "value": "0x9ace6120DED418589593C5D953e8F6778Ed94b6c", "valueDecoded": null },
    { "name": "_tokenId", "type": "uint256", "value": "1", "valueDecoded": null },
    { "name": "_amount", "type": "uint256", "value": "1", "valueDecoded": null }
  ],
  "accuracy": "ONLY_FUNCTION_MATCH"
}

2. Call the safe-transaction-service:

curl -X POST "https://api.safe.global/tx-service/sep/api/v1/data-decoder/" \
  -H "Content-Type: application/json" \
  -d '{
    "data": "0x156e29f60000000000000000000000009ace6120ded418589593c5d953e8f6778ed94b6c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001",
    "to": "0x9ace6120DED418589593C5D953e8F6778Ed94b6c"
  }'

Response (also incorrect, different fallback match):

{
  "method": "mint",
  "parameters": [
    { "name": "to", "type": "address", "value": "0x9ace6120DED418589593C5D953e8F6778Ed94b6c" },
    { "name": "payAmount", "type": "uint256", "value": "1" },
    { "name": "minReceive", "type": "uint256", "value": "1" }
  ]
}

3. Verify that Etherscan has the correct proxy relationship:

curl "https://api.etherscan.io/v2/api?chainid=11155111&module=contract&action=getsourcecode&address=0x9ace6120DED418589593C5D953e8F6778Ed94b6c&apikey=YOUR_KEY"

Returns:

  • ContractName: TransparentUpgradeableProxy
  • Proxy: 1
  • Implementation: 0xdc220588726d033e9a662ec7c29641f725c67254

4. Verify that the implementation's ABI on Etherscan has the correct mint function:

curl "https://api.etherscan.io/v2/api?chainid=11155111&module=contract&action=getabi&address=0xDc220588726d033E9a662ec7c29641F725c67254&apikey=YOUR_KEY"

Returns ComplianceToken ABI which includes:

{
  "name": "mint",
  "inputs": [
    { "name": "depositAddr", "type": "address" },
    { "name": "amount", "type": "uint256" },
    { "name": "validUntil", "type": "uint256" }
  ]
}

Expected behavior

The decoder should return the implementation contract's parameter names:

{
  "method": "mint",
  "parameters": [
    { "name": "depositAddr", "type": "address", "value": "0x9ace6120DED418589593C5D953e8F6778Ed94b6c" },
    { "name": "amount", "type": "uint256", "value": "1" },
    { "name": "validUntil", "type": "uint256", "value": "1" }
  ],
  "accuracy": "FULL_MATCH"
}

The expected flow:

  1. Fetch ABI for proxy address via getabi → gets TransparentUpgradeableProxy ABI (no application functions)
  2. Detect that this is a proxy via getsourcecodeProxy: 1, Implementation: 0xdc22...7254
  3. Fetch the implementation's ABI via getabi for 0xdc22...7254 → gets ComplianceToken ABI with the correct mint function
  4. Use the implementation ABI to decode the transaction

Environment

Additional context

Why 4-byte fallback gives wrong results

The function selector 0x156e29f6 = mint(address,uint256,uint256) exists in multiple contracts with different parameter names:

Source Parameter names
ComplianceToken (correct) depositAddr, amount, validUntil
ERC1155 (4-byte DB match) _to, _tokenId, _amount
Other (4-byte DB match) to, payAmount, minReceive

Because the decoder doesn't resolve the proxy → implementation ABI chain, it falls back to the 4-byte signature database and picks a wrong match.

Etherscan getabi vs getsourcecode behavior for proxies

API Returns for proxy address
getabi TransparentUpgradeableProxy ABI only (constructor, events, errors — no application functions)
getsourcecode Proxy: 1, Implementation: 0xdc22...7254contains the link to the implementation

The decoder currently appears to only use getabi, which misses the implementation ABI entirely.

Impact

This affects all TransparentUpgradeableProxy contracts on Safe Wallet. Users see wrong parameter names in the transaction confirmation screen, which is a security/UX concern — users cannot verify they are signing the correct parameters.

Suggested fix

When fetching contract metadata, if getsourcecode returns Proxy: 1 with a non-empty Implementation address:

  1. Fetch the implementation's ABI via getabi(implementation_address)
  2. Use the implementation ABI (or merge it with the proxy ABI) for decoding transactions targeting the proxy address

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions