Vyper: Here be Snakes!
“Vyper is beta software, use with care”
These words are included in bold in Vyper’s GitHub repository. In the constantly-shifting world of web3, disclaimers like this are so commonplace they hardly register as a warning anymore.
As development on Vyper picks up steam, the language’s vocal focus on security and auditability are attracting more and more developers. At Consensys Diligence, we’ve worked with a few clients now that have used Vyper for their smart contracts (see the Uniswap audit and blog post), and I have no doubt we’ll see more in the future. Compared to Solidity, Vyper’s restricted set of features and familiar syntax make it a natural choice for many use-cases. But before you throw your pragma solidity
s out the window, dear reader, consider the following…
Something Askew
Here’s a small contract to read through:
owner: public(address)
@public
@payable
def __init__():
assert msg.value == as_wei_value(1, "ether")
self.owner = msg.sender
@public
@payable
def blockHashAskewLimitary(value: uint256):
send(self.owner, self.balance)
@public
@payable
def __default__():
assert msg.value == as_wei_value(1, "ether")
send(msg.sender, self.balance)
Short and sweet. There are only a few functions:
-
__init__()
works just like a Solidity constructor. It’s run a single time during deployment and cannot be accessed once deployed. It requires the sender to fund the contract with an initial1 ETH
. It sets the sender as theowner
. -
blockHashAskewLimitary(uint)
sends the contract’s owner the entire balance of the contract. -
__default__()
is analogous to Solidity’s fallback functions. It’s meant to run if the function selector passed in to calldata doesn’t match one of the other functions in the contract. In this example, the fallback requires you to send1 ETH
to the contract, and sends you the entire balance in return. Note that if there is already1 ETH
in the contract andmsg.value
is1 ETH
,send(msg.sender, self.balance)
will award the caller2 ETH
. Profit. Nice.
Did you spot the bug? It’s a tough one—the logic in the contract is sound. But as it turns out… the __default__
function doesn’t exist.
This strange omission results from an edge case in Vyper’s function selector collision detection. Contracts use function selectors to determine which public function should be executed; each public function has a function selector. In this contract, blockHashAskewLimitary
has a function selector of 0x00000000
:
bytes4(keccak256("blockHashAskewLimitary(uint256)"))
This weird function selector (which I found via https://www.4byte.directory/) ends up overriding the contract’s fallback function. From Solidity’s documentation:
Fallback Function
A contract can have exactly one unnamed function. This function cannot have arguments, cannot return anything and has to have external visibility. It is executed on a call to the contract if none of the other functions match the given function identifier (or if no data was supplied at all).
In this case, no data was supplied to the contract. But rather than invoke the fallback, the contract will always invoke blockHashAskewLimitary
, falsely interpreting “no calldata” as “zeroed out calldata”. This means that a call to the contract with no calldata and including 1 ETH
ends up invoking blockHashAskewLimitary
, sending the ether straight to the owner. Classic honeypot.
Time to fall back to Solidity?
Is Vyper’s goal of security and auditability a pipe dream? Should we avoid it at all costs? Put down your pitchforks!
As discussed before, Vyper is beta software. A fundamental focus of the language is auditability—and to that end, the much smaller type system, bounds and overflow checking on array access and arithmetic, and lack of inheritance definitively succeed in creating a simple, readable language. For many use-cases, Vyper will likely make sense over Solidity!
On the other hand, Solidity’s low-level features empower developers to make better use of the EVM’s full capability. As with most decisions in software development, there are tradeoffs with each option and it’s up to the developer to determine what works best for them.
For now, though, Solidity has a distinct advantage over Vyper: it’s battle-tested. Solidity has been around for a (relatively) long time. It’s been audited, it’s widely used, it’s well-supported by tools and frameworks, and auditors generally understand it very well. I sometimes get the sense when debugging Solidity that the compiler won’t even let you touch your nose without first checking to see that you have one.
Solidity has these checks in place because it’s been used, and because that use has uncovered tons of problems. Take a look at the list of known Solidity bugs: https://solidity.readthedocs.io/en/v0.5.11/bugs.html. A storied history!
(This issue was initially reported privately, and has since been addressed.)
Summary
- Take the time to understand both languages - neither exists in isolation and contract-to-contract interaction may require knowledge of both.
- When using beta software, always keep in mind… here be snakes!
Thinking about smart contract security? We can provide training, ongoing advice, and smart contract auditing. Contact us.