solidity
solidity copied to clipboard
Ability to override function selectors to support external interfaces not expressible in valid syntax.
Came up in https://github.com/ethereum/solidity/issues/11743, earlier in https://github.com/ethereum/solidity/issues/1687, resp. https://github.com/ethereum/solidity/issues/8081
We need a way to override function selectors in interfaces for two reasons:
- We might ourselves add new keywords to the language which might break backwards compatibility with contracts with an entry point with that name.
- Third-Party languages might allow selectors that collide with keywords or are otherwise incompatible with regular solidity function definitions.
We seem to have pretty much consensus on that we want this, but we require good syntax for it.
One option (I myself dislike) would be a modifier-style decorator at various positions with various names.
interface C
{
function errorCompat() selector(0x01020304) external returns (uint);
function errorCompat2() signature("error()") external returns (uint);
}
To me it seems like those are not prominent enough, though, among visiblity and mutability.
We had the idea to use = Base.f;
for disambiguating during inheritance at some point, along those lines a possiblity would be:
interface C
{
function errorCompat() external returns (uint) = "error()";
function errorCompat2() external returns (uint) = 0x01020304;
}
Might be good to force encapsulation in a call-like construct, e.g. = externalSignature("error()")
, but choosing a name for that may be hairy.
Another option would be to borrow from the call option syntax, e.g.
interface C
{
function{ signature = "error()" } errorCompat() external returns (uint);
function{ selector = 0x01020304 } errorCompat() external returns (uint);
}
advantage: extensible if we ever need further similar annotations (e.g. if we ever want to break the ABI encoding, we could select a version here). No need for making selector
(or anything) a keyword.
I think the last option may work the best for us given we already use that syntax. It does look a bit odd on the declaration site though.
As discussed on gitter, this could be reused for introduced a new ABI encoding (which came up several times in the past), via:
interface I {
function{abi=3} transferTo(address to, uint amount) external returns(uint);
}
This works well for calling such interfaces, but what should happen in contracts implementing such functions -- they should just copy the declaration as-is, including the abi
part.
Shouldn't it be on the method though?
interface I {
function transferTo{ abi = 3 }(address to, uint amount) external returns(uint);
function errorCompat{ signature = "error()" }() external returns (uint);
function errorCompat{ selector = 0x01020304 }() external returns (uint);
}
Should also work on the method, but aesthetically to me on function
seems nicer... and conceptually both works in my mind.
@axic Noted that there may also be the need to implement functions with inexpressible selectors. (EDIT: ah, sorry, I only see now that you already commented on that above yourself)
The bracket syntax should work for that, too, though.
contract C {
function{selector= ...} f() external {}
}
But I'd tend to still restrict this to interfaces, when inheriting and require that there is a unique interface containing that function. Although we might force to re-state the selector on implementation nonetheless.
Further issues: overloading, i.e.
function{signature = "f()"} f() external;
function{signature = "g()"} f(uint256) external;
first response: disallow entirely - there must be a unique signature in solidity for each "artificial signature"
conflicting signatures
function{signature = "f()"} g() external;
function{signature = "f()"} h() external;
or
function{signature = "f()"} g() external;
function f() external;
first response: should be covered by our analysis of uniqueness of selectors.
Next question: what to do with these things in json ABI. For signature = "someSig"
, it would just appear as someSig
.
But selector=...
might be an issue.
Another question is regarding the ABI JSON. If we always want to be able to generate an output from an interface, then we should not allow selector
, just signature
, to make the problem go away.
Another question is regarding the ABI JSON. If we always want to be able to generate an output from an interface, then we should not allow
selector
, justsignature
, to make the problem go away.
At least that's an option. We could also extend the ABI JSON to support numeric selectors, but that would raise a whole new bunch of questions, so at least at first restricting to signatures may be wise indeed.
Another thought: Since the bracket syntax already exists for function calls, we could force re-mentioning the signature on all call sites, if we consider obscuring what someInterface.someFunction()
actually calls an issue.
There may also be more corner-cases to consider, e.g. unicode escapes in the signature string.
An entirely different (reasonably insane) approach would be to allow quoted strings as identifiers in function context :-D.
interface I {
function "error()"() external;
}
contract C {
function f(I i) public { i."error()"(); }
}
Another general choise is: do we want to be able to override the full signature or just the name or both. (Since other languages may have different ABI types full signatures seem more general).
There may also be more corner-cases to consider, e.g. unicode escapes in the signature string.
Do not 😅
How about this syntax:
interface I {
function "error" as err() external;
}
contract C {
function f(I i) public { i.err(); }
}
This would follow how as
is used e.g. in imports - the original name is the thing before as
but only the identifier after it is visible in the source unit. The only weirdness being that the thing before as
here is a string rather than an identifier.
Regarding overloading, I think that we'll have to consider both names for collisions. I.e. if you declare function "g" as f(uint)
then both function f(uint)
and function g(uint)
should not be allowed due to a conflicting signature.
Another general choise is: do we want to be able to override the full signature or just the name or both. (Since other languages may have different ABI types full signatures seem more general).
I think that it's important to allow overriding just the name, without having to repeat the signature. We might allow overriding the whole signature too but I'd consider that optional.
I'd disallow (
and )
in function names used in signatures to avoid ambiguities. It would also allow us to have the same syntax for both cases - function "g(uint256,bool)" as f(uint, bool)
would change the whole signature while function "g" as f(uint, bool)
would only change the name.
The problem with overriding the signature though is that it would open a whole new bag of potential mistakes if it's not validated. Forgetting to update the signature after updating the function, introducing whitespace where it's not expected, etc. And validating it would be problematic if the point is to allow things that the compiler does not support.
There may also be more corner-cases to consider, e.g. unicode escapes in the signature string.
Do not sweat_smile
Yeah, I'd rather disallow non-ASCII chars. It people suddenly start hand-crafting tons of contracts that do have unicode in signatures (or some other language starts allowing them), it might force us to support them but I'd worry about that when it happens :) I'd disallow control characters as well.
Actually, maybe it would be better to require these names to be valid identifiers? It would be enough to solve the keyword problem for now and we can always extend the set of supported characters later in a non-breaking way.
I think I like the {signature = "..."}
syntax most, but I would prefer to not have the option to allow to change the selector. Also, do we want to perform any type checking on the parameters? Can we do it?
I'd not do any type checking and leave things entirely in user-responsibility - that way we simultaneously achieve compatibility with types other languages might have, but we don't (or ABI types we might want to remove at some point, if that ever comes up ;-)). But I'd also be fine with doing type checking, since we can still relax it afterwards. EDIT: there might even be cases in which some other language has some type that extends to two 256-bit words, which could be captures in a struct in solidity or in two separate arguments, so I'd not even attempt to verify that the signature has the same number of arguments - or even that it has parentheses.
Comments from call: We should use :
instead of =
and we should also allow this for function types.
Ok actually not needed for function types.
@cameel proposed to move it after the function definition into the specifiers space. I think there it could use some syntax similar to modifiers:
function transferTo(address to, uint amount) abi({ version: 3, selector: 0x01020304 }) external returns(uint);
function transferTo(address to, uint amount) abi{ version: 3, selector: 0x01020304 } external returns(uint);
One important detail that needs to be taken into account here: the syntax has to work for public state variables too since these generate getter functions.
@cameel Continuing our discussion here. Sorry, I might be missing something, but what’s wrong with:
pragma solidity 0.8.11;
contract Test {
error Oops(uint256 value);
function error() external pure returns (uint256) { return 123; }
}
?
Nothing yet, but we'd like to make it a keyword in 0.9.0. Then it will break. We almost did but had to revert that change (#11218, #11859).