Search code examples
ethereumsoliditydecodesmartcontractsabi

Solidity how to validate calldata decodes to a paticular struct


I have an interface in solidity which looks like so, I would like my call to revert if the calldata passed isn't of a specific type

// Resolver interface
interface IResolver {
    // Pass payment info as calldata only the manager should have the right to update it
    function resolve(uint256 amount, ResolverOptions calldata resolverOptions) external returns (uint256);

    // Reverts if the calldata passes is not a proper struct
   function validateAdditionalCalldata(bytes calldata additionalCalldata) external view;
}

I've created a class to implement this here:

struct fooResolverOptions {
    address[] fooAddresses; 
    uint256[] fooAmounts; 
}

contract FooResolver is IResolver {

    // Validate the additional calldata passed to the resolver contract
    function validateAdditionalCalldata(bytes calldata additionalCalldata) view external {
        // Convert the additional calldata to bytes memory
        bytes memory additionalCalldataMemory = additionalCalldata;

        // Decode the additional calldata as a FooResolverOptions struct
        FooResolverOptions memory fooOptions;
        bool success = abi.decode(additionalCalldataMemory, fooOptions);
    

        // Check if the decode was successful
        require(success, "Invalid additional calldata");
    }
}

None of the Way's I've tried to decode work:

bool success = abi.decode(additionalCalldataMemory, fooOptions);

this way claims there is no return value from decode.

FooResolverOptions memory fooOptions;
abi.decode(additionalCalldata, fooOptions);

This way claims it wants a tuple of types. How do I decode a struct data, and validate it succeeded?


Solution

  • Solidity currently (v0.8) doesn't support dynamic arguments in abi.decode(), so you'll need to write logic that validates against predefined set of types.


    bytes calldata additionalCalldata in your example is an array of bytes, so abi.decode(additionalCalldataMemory, <types>); tries to decode the binary to whatever <types> you pass. If the input fits the type length, it will simply decode the value to the type.

    Example where the value fits into both bool and address types, so both operations succeed:

    function validateAdditionalCalldata() pure external returns (bool, address) {
        bytes memory additionalCalldataMemory = hex"0000000000000000000000000000000000000000000000000000000000000001";
        bool decoded1 = abi.decode(additionalCalldataMemory, (bool));
        address decoded2 = abi.decode(additionalCalldataMemory, (address));
    
        return (decoded1, decoded2);
    }
    

    When the value doesn't fit the type, it throws an exception. Uncaught exception effectively reverts the transaction or the call. However, you can use try / catch to catch the exception.

    pragma solidity ^0.8;
    
    contract FooResolver {
        function validateAdditionalCalldata() external view returns (bool, address) {
            // does not fit `bool` but still fits `address`
            bytes memory additionalCalldataMemory = hex"0000000000000000000000000000000000000000000000000000000000000002";
    
            bool decoded1;
            try this.decodeToBool(additionalCalldataMemory) returns (bool decodedValue) {
                decoded1 = decodedValue;
            } catch {
                decoded1 = false;
            }
    
            address decoded2 = abi.decode(additionalCalldataMemory, (address));
    
            return (decoded1, decoded2);
        }
    
        // workaround - try/catch can be currently (v0.8) used on external function calls - but not on native function calls
        function decodeToBool(bytes memory data) external pure returns (bool) {
            return abi.decode(data, (bool));
        }
    }