See how vyperlang compares to other vendors in security performance
vyper is a Pythonic Smart Contract Language for the EVM. Vyper sqrt() builtin uses the babylonian method to calculate square roots of decimals. Unfortunately, improper handling of the oscillating final states may lead to sqrt incorrectly returning rounded up results. This issue is being addressed and a fix is expected in version 0.4.1. Users are advised to upgrade as soon as the patched release is available. There are no known workarounds for this vulnerability.
Multiple evaluation of a single expression is possible in the iterator target of a for loop. While the iterator expression cannot produce multiple writes, it can consume side effects produced in the loop body (e.g. read a storage variable updated in the loop body) and thus lead to unexpected program behavior. Specifically, reads in iterators which contain an ifexp (e.g. for s: uint256 in ([read(), read()] if True else [])) may interleave reads with writes in the loop body.
The fix is tracked in https://github.com/vyperlang/vyper/pull/4488.
Vulnerability Details
Vyper for loops allow two kinds of iterator targets, namely the range() builtin and an iterable type, like SArray and DArray.
During codegen, iterable lists are required to not produce any side-effects (in the following code, rangescope forces iterlist to be parsed in a constant context, which is checked against isconstant).
python def parseForlist(self): with self.context.rangescope(): iterlist = Expr(self.stmt.iter, self.context).irnode ...
def rangescope(self): prevvalue = self.inrangeexpr self.inrangeexpr = True yield self.inrangeexpr = prevvalue
def isconstant(self): return self.constancy is Constancy.Constant or self.inrangeexpr
However, this does not prevent the iterator from consuming side effects provided by the body of the loop. For dynamic arrays, the compiler simply panics: vyper x: DynArray[uint256, 3]
@external def test(): for i: uint256 in (self.usesideeffect() if True else self.usesideeffect()): pass
@view def usesideeffect() -> DynArray[uint256, 3]: return self.x
For SArrays on the other hand, iterlist is instantiated in the body of a repeat ir, so it can be evaluated several times.
Here are three illustrating examples. In the first example, the following test case pre-evaluates the iter list and stores the result to a temporary list in memory. So the list is only evaluated once, before entry into the loop body, and the log output will be 0, 0, 0. vyper event I: i: uint256
x: uint256
@deploy def init(): self.x = 0
@external def test(): for i: uint256 in [self.usesideeffect(), self.usesideeffect(), self.usesideeffect()]: self.x += 1 log I(i)
@view def usesideeffect() -> uint256: return self.x
However, in the next two examples, because the iterator target is not a list literal, it will be evaluated in the loop body. In the second example, iterlist is an ifexp, thus it will be evaluated lazily in the loop body. The log output will be 0, 1, 2 due to consumption of side effects.
vyper event I: i: uint256
x: uint256
@deploy def init(): self.x = 0
@external def test(): for i: uint256 in ([self.usesideeffect(), self.usesideeffect(), self.usesideeffect()] if True else self.otherclause()): self.x += 1 log I(i)
@view def usesideeffect() -> uint256: return self.x
@view def otherclause() -> uint256[3]: return [0, 0, 0]
In the third example, iterlist is also an ifexp, thus it will only be evaluated in the loop body. The log output will be 0, 1, 2 due to consumption of side effects.
vyper event I: i: uint256
x: uint256[3]
@deploy def init(): self.x = [0, 0, 0]
@external def test(): for i: uint256 in (self.usesideeffect() if True else self.otherclause()): self.x[0] += 1 self.x[1] += 1 self.x[2] += 1 log I(i)
@view def usesideeffect() -> uint256[3]: return self.x
@view def otherclause() -> uint256[3]: return [0, 0, 0]
Vyper handles AugAssign statements by first caching the target location to avoid double evaluation. However, in the case when target is an access to a DynArray and the rhs modifies the array, the cached target will evaluate first, and the bounds check will not be re-evaluated during the write portion of the statement. In other words, the following code
vyper def poc(): a: DynArray[uint256, 2] = [1, 2] a[1] += a.pop()
is equivalent to: vyper def poc(): a: DynArray[uint256, 2] = [1, 2] a[1] += a[len(a) - 1] a.pop() rather than: vyper def poc(): a: DynArray[uint256, 2] = [1, 2] s: uint256 = a[1] t: uint256 = a.pop() a[1] = s + t # reverts due to oob access
Summary
When the Vyper Compiler uses the precompiles EcRecover (0x1) and Identity (0x4), the success flag of the call is not checked. As a consequence an attacker can provide a specific amount of gas to make these calls fail but let the overall execution continue. Then the execution result can be incorrect.
Based on EVM's rules, after the failed precompile the remaining code has only 1/64 of the pre-call-gas left (as 63/64 were forwarded and spent). Hence, only fairly simple executions can follow the failed precompile calls. Therefore, we found no significantly impacted real-world contracts.
The fix is tracked in https://github.com/vyperlang/vyper/pull/4451.
Details
The relevant precompiles
EcRecover
EcRecover is used in vyper's ecrecover built-in. As the precompile consumes 3000 gas, any execution after an out-of-gas EcRecover call has at most 47 gas left.
Identity
- The Identity precompile is used in vyper to perform memory copy operations. As its cost is variable, a variable amount of gas might be left after a failed call. The bigger the copy operation, the more gas can be left. Hence, a failed call to Identity could theoretically be followed by successful storage changes or emitted events. - Identity is no longer used when evm-version cancun is used (because MCOPY is used instead). In 0.4.0 cancun is default, in 0.3.10 cancun is an option, otherwise cancun is not available. As only pre-cancun versions are relevant, we don't have to consider transient storage operations succeeding a failed call to Identity.
The other precompiles
- Calls to Sha2, ecAdd, and ecMul have success checks and have had them for a long time. - The precompiles modexp, ripe, blake, ecPairing, and Point Evaluation have no builtins in vyper.
PoC
In the following we provide concrete examples of incorrectly generated bytecode. These examples are not optimized, but rather Proof-of-Concepts. The list is also not exhaustive.
ecrecover use
- Affected versions: 0.2.0 - 0.4.0 - For older compiler versions (<=0.3.9) it behaves similarly to this older advisory. As no data is returned, the previous value of the memory word is returned to the user. Hence, any dirty bytes might be returned. Contracts with older compiler versions and ecrecover were checked. - For new vyper versions, the output buffer is zeroed, so when the call fails zero is returned. This is an incorrect result, but developers should anyway check for 0 as a failure case. Hence, this is unlikely to result in issues. However, we did search for such cases. - As mentioned above at most 47 gas is left after the failed call, hence a return is the most realistic scenario to be attacked.
Vulnerable Code: py @external @view def foo(hash: bytes32, v: uint256, r:uint256, s:uint256) -> address: return ecrecover(hash, v, r, s)
Problematic Call: py print( c.foo( binascii.unhexlify( "6c9c5e133b8aafb2ea74f524a5263495e7ae5701c7248805f7b511d973dc7055" ), 28, 78616903610408968922803823221221116251138855211764625814919875002740131251724, 37668412420813231458864536126575229553064045345107737433087067088194345044408, ) ) # Returns 0x9eE53ad38Bb67d745223a4257D7d48cE973FeB7A
print( c.foo( binascii.unhexlify( "6c9c5e133b8aafb2ea74f524a5263495e7ae5701c7248805f7b511d973dc7055" ), 28, 78616903610408968922803823221221116251138855211764625814919875002740131251724, 37668412420813231458864536126575229553064045345107737433087067088194345044408, gas=3000, ) ) # Returns 0x0000000000000000000000000000000000000000
Identity to copy Dynamic Arrays
- Affected versions: 0.3.2 - 0.3.9 - Dynamic Arrays might be copied on different occasions - That copy operation can fail leading to incorrect accesses afterwards
Vulnerable Code: py
@external def foo() -> uint256: a: DynArray[uint256, 4000] = [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] b: DynArray[uint256, 4000] = a return b[0]
Problematic Call: py print(c.foo()) # Prints 2 print(c.foo(gas=170000)) # Prints 0
Identity in ABI Encoding of Returndata
- Affected versions: 0.3.2 - 0.4.0 - Complex types such as Dynamic array with Strings inside need to be abi encoded before being returned - In that return there is a memory copy which can fail
Vulnerable Code: py @external @view def foo(x: String[1000000], y: String[1000000]) -> DynArray[String[1000000], 2]: z: DynArray[String[1000000], 2] = [x, y] # Some code return z
Problematic Call: py calldata0 = "a"10 calldata1 = "b"1000000 c.foo(calldata0, calldata1) # Returns correct data c.foo(calldata0, calldata1, gas=48400000) # Returns incorrect data (only first part)
Assertion based on data copied through Identity
- Affected versions: 0.2.0 - 0.4.0 - An incomplete copy operation might falsify the result of a subsequent assert
Vulnerable Code: py @internal def bar() -> uint256[3000]: a: uint256[3000] = [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] return a
@external def foo(): s: uint256[3000] = self.bar() assert(s[0] == 0)
Problematic Call: py try: c.foo() # Correctly reverts except Exception as e: print("Correctly reverted") try: c.foo(gas=210000) # Incorrectly succeeds print("Incorrectly succeeded") except Exception: pass
Identity used in rawrevert
- Affected versions: 0.3.8 - 0.4.0 - A copy operation might appear as part of rawrevert - As a result the revert reason might be incorrect
Vulnerable Code: py @external def foo(data: Bytes[10000]): b: Bytes[10000] = data rawrevert(b)
Problematic Call: py calldata = binascii.unhexlify("bb" 10000) c.foo(calldata) # Has correct revert reason c.foo(calldata, gas=4800) # Has empty revert reason, but not out-of-gas
Identity to copy static arrays
- Affected versions: 0.2.0 - 0.4.0 - Vyper might perform a memory copy for static arrays, e.g. when moving them in and out of internal functions - As this access is especially cheap (due to the static checks) it can also happen for smaller sizes
Vulnerable Code: py @external def foo(x: uint256[2500]) -> uint256: s: uint256[2500] = x t: uint256[2500] = s return t[0]
Problematic Call: py calldata = [2] + [0] 2499 print(c.foo(calldata)) # Prints 2 print(c.foo(calldata, gas=74500)) # Prints 0
Identity to copy and return String or Bytes
- Affected versions: 0.20 - 0.4.0 - Multiple situations in vyper might trigger a memory copy operation, e.g. moving data in and out of internal functions - If the target buffer is later returned, incorrect data might be returned
Vulnerable Code: py @external @view def foo(x: String[1000000]) -> String[1000000]: return x
Problematic Call: py calldata = "a"1000000 x = c.foo(calldata) # Returns calldata y = c.foo(calldata, gas=8000000) # Returns empty data
Identity and accessing the length of the target data
- Affected versions: 0.3.10 - 0.4.0 - Accessing the data is fairly cheap, making it possible for smaller data copies
Vulnerable Code: py @external @view def foo(x: String[1000000]) -> uint256: y: String[1000000] = x return len(y)
Problematic Call: py calldata = "a"1000000 x = c.foo(calldata) # Returns correct length y = c.foo(calldata, gas=7929200) # Returns incorrect length
Identity to copy and return String or Bytes
- Affected versions: 0.3.10 - 0.4.0 - Multiple situations in vyper might trigger a memory copy operation, e.g. moving data in and out of internal functions - If the target buffer is later returned, incorrect data might be returned
Vulnerable Code: py @external @view def foo(x: String[1000000]) -> String[1000000]: return x
Problematic Call: py calldata = "a"1000000 x = c.foo(calldata) # Returns calldata y = c.foo(calldata, gas=8000000) # Returns empty data
Impact
A contract search was conducted and yielded no significant results.
The advisory was rated a medium because the likelihood is low, but difficult to detect by source code analysis alone, and could yield unexpected results if a contract is affected by the bug.
Summary Using the sqrt builtin can result in multiple eval evaluation of side effects when the argument has side-effects. The bug is more difficult (but not impossible!) to trigger as of 0.3.4, when the unique symbol fence was introduced (https://github.com/vyperlang/vyper/pull/2914).
A contract search was performed and no vulnerable contracts were found in production.
Details It can be seen that the buildIR function of the sqrt builtin doesn't cache the argument to the stack: https://github.com/vyperlang/vyper/blob/4595938734d9988f8e46e8df38049ae0559abedb/vyper/builtins/functions.py#L2151
As such, it can be evaluated multiple times (instead of retrieving the value from the stack).
PoC With at least Vyper version 0.2.15+commit.6e7dba7 the following contract: vyper c: uint256
@internal def somedecimal() -> decimal: self.c += 1 return 1.0
@external def foo() -> uint256: k: decimal = sqrt(self.somedecimal()) return self.c passes the following test: solidity // SPDX-License-Identifier: MIT pragma solidity >=0.8.13;
import "../../lib/ds-test/test.sol"; import "../../lib/utils/Console.sol"; import "../../lib/utils/VyperDeployer.sol";
import "../ITest.sol";
contract ConTest is DSTest { VyperDeployer vyperDeployer = new VyperDeployer();
ITest t;
function setUp() public { t = ITest(vyperDeployer.deployContract("Test")); }
function testFoo() public { uint256 val = t.foo(); console.log(val); assert (val == 4); } } Patches Patched in https://github.com/vyperlang/vyper/pull/3976.
Impact No vulnerable production contracts were found.
Summary Prior to v0.3.0, default() functions did not respect the @nonreentrancy decorator and the lock was not emitted. This is a known bug and was already visible in the issue tracker (https://github.com/vyperlang/vyper/issues/2455), but it is being re-issued as an advisory so that tools relying on the advisory publication list can incorporate it into their searches.
A contract search was additionally performed and no vulnerable contracts were found in production.
PoC vyper @external @payable @nonreentrant("default") def default(): pass
after codegen: [seq, [if, [lt, calldatasize, 4], [goto, fallback]], [mstore, 28, [calldataload, 0]], [with, funcsig, [mload, 0], seq], [sequnchecked, [label, fallback], [seq, pass, # Line 5 pass, pass, # Line 4 stop]]],
Impact No vulnerable production contracts were found. Additionally, using a lock on a default function is a very sparsely used pattern. As such, the impact is low.
Summary Using the createfromblueprint builtin can result in a double eval vulnerability when rawargs=True and the args argument has side-effects.
A contract search was performed and no vulnerable contracts were found in production. In particular, the rawargs variant of createfromblueprint was not found to be used in production.
Details It can be seen that the buildcreateIR function of the createfromblueprint builtin doesn't cache the mentioned args argument to the stack: https://github.com/vyperlang/vyper/blob/cedf7087e68e67c7bfbd47ae95dcb16b81ad2e02/vyper/builtins/functions.py#L1847
As such, it can be evaluated multiple times (instead of retrieving the value from the stack).
PoC The vulnerability is demonstrated in the following boa test: vyper src1 = """ c: uint256 """ deployer = """ createdaddress: public(address) deployed: public(uint256)
@external def get() -> Bytes[32]: self.deployed += 1 return b''
@external def create(target: address): self.createdaddress = createfromblueprint(target, rawcall(self, methodid("get()"), maxoutsize=32), rawargs=True, codeoffset=3) """
Factory = b.loadspartial(src1) c = Factory.deployasblueprint()
c2 = b.loads(deployer, b'') c2.create(c) c2.deployed() The output of c2.deployed() is 2 although create was called only once and the value was initialized to 0.
Patches Patched in https://github.com/vyperlang/vyper/pull/3976.
Impact No vulnerable production contracts were found. Additionally, double evaluation of side-effects should be easily discoverable in client tests. As such, the impact is low.
Summary Using the slice builtin can result in a double eval vulnerability when the buffer argument is either msg.data, self.code or <address>.code and either the start or length arguments have side-effects.
A contract search was performed and no vulnerable contracts were found in production. Having side-effects in the start and length patterns is also an unusual pattern which is not that likely to show up in user code. It is also much harder (but not impossible!) to trigger the bug since 0.3.4 since the unique symbol fence was introduced (https://github.com/vyperlang/vyper/pull/2914).
Details It can be seen that the buildadhocslicenode function of the slice builtin doesn't cache the mentioned arguments to the stack: https://github.com/vyperlang/vyper/blob/4595938734d9988f8e46e8df38049ae0559abedb/vyper/builtins/functions.py#L244
As such, they can be evaluated multiple times (instead of retrieving the value from the stack).
PoC with Vyper version 0.3.3+commit.48e326f the call to foo passes the asserts: vyper l: DynArray[uint256, 10]
@external def foo(cs: String[64]) -> uint256: for i in range(10): self.l.append(1) assert len(self.l) == 10 s: Bytes[64] = b"" s = slice(msg.data, self.l.pop(), 3) assert len(self.l) == 10 - 2 return len(self.l)
Patches Patched in https://github.com/vyperlang/vyper/pull/3976.
Impact No vulnerable production contracts were found.
Summary Incorrect values can be logged when rawlog builtin is called with memory or storage arguments to be used as topics.
A contract search was performed and no vulnerable contracts were found in production. In particular, no uses of rawlog() were found at all in production; it is apparently not a well-known function.
Details The buildIR function of the RawLog class fails to properly unwrap the variables provided as topics. Consequently, incorrect values are logged as topics.
PoC vyper x: bytes32
@external def f(): self.x = 0x1234567890123456789012345678901234567890123456789012345678901234 rawlog([self.x], b"") # LOG1(offset:0x60, size:0x00, topic1:0x00)
y: bytes32 = 0x1234567890123456789012345678901234567890123456789012345678901234 rawlog([y], b"") # LOG1(offset:0x80, size:0x00, topic1:0x40) Patches Fixed in https://github.com/vyperlang/vyper/pull/3977.
Impact Incorrect values can be logged which may result in unexpected behavior in client-side applications relying on these logs.
Summary
When looping over a range of the form range(start, start + N), if start is negative, the execution will always revert. Details
This issue is caused by an incorrect assertion inserted by the code generation of the range (stmt.parseForrange()):
https://github.com/vyperlang/vyper/blob/9136169468f317a53b4e7448389aa315f90b95ba/vyper/codegen/stmt.py#L286-L287
This assertion was introduced in https://github.com/vyperlang/vyper/commit/3de1415ee77a9244eb04bdb695e249d3ec9ed868 to fix https://github.com/advisories/GHSA-6r8q-pfpv-7cgj. The issue arises when start is signed, instead of using sle, le is used and start is interpreted as an unsigned integer for the comparison. If it is a negative number, its 255th bit is set to 1 and is hence interpreted as a very large unsigned integer making the assertion always fail. PoC
Vyper @external def foo(): x:int256 = minvalue(int256) # revert when it should not since we have the following assertion that fails: # [assert, [le, minvalue(int256), maxvalue(int256) + 1 - 10]], for i in range(x, x + 10): pass
Patches
patched in v0.4.0, specifically, https://github.com/vyperlang/vyper/pull/3679 disallows this form of range().
Impact
Any contract having a range(start, start + N) where start is a signed integer with the possibility for start to be negative is affected. If a call goes through the loop while supplying a negative start the execution will revert.
Summary
When using the built-in extract32(b, start), if the start index provided has for side effect to update b, the byte array to extract 32 bytes from, it could be that some dirty memory is read and returned by extract32.
As of v0.4.0 (specifically, commit https://github.com/vyperlang/vyper/commit/3d9c537142fb99b2672f21e2057f5f202cde194f), the compiler will panic instead of generating bytecode.
Details
Before evaluating start, the function Extract32.buildIR caches only:
- The pointer in memory/storage to b: https://github.com/vyperlang/vyper/blob/10564dcc37756f3d3684b7a91fd8f4325a38c4d8/vyper/builtins/functions.py#L916-L918 - The length of b: https://github.com/vyperlang/vyper/blob/10564dcc37756f3d3684b7a91fd8f4325a38c4d8/vyper/builtins/functions.py#L920-L922
but do not cache the actual content of b. This means that if the evaluation of start changes b's content and length, an outdated length will be used with the new content when extracting 32 bytes from b.
PoC
Calling the function foo of the following contract returns b'uuuuuuuuuuuuuuuuuuuuuuuuuuu\x00\x00789' meaning that extract32 accessed some dirty memory.
Vyper var:Bytes[96]
@internal def bar() -> uint256: self.var = b'uuuuuuuuuuuuuuuuuuuuuuuuuuuuuu' self.var = b'' return 3
@external def foo() -> bytes32: self.var = b'abcdefghijklmnopqrstuvwxyz123456789' return extract32(self.var, self.bar(), outputtype=bytes32) # returns b'uuuuuuuuuuuuuuuuuuuuuuuuuuu\x00\x00789'
Impact
For contracts that are affected, it means that calling extract32 returns dirty memory bytes instead of some expected output.
Summary
If an excessively large value is specified as the starting index for an array in abidecode, it can cause the read position to overflow. This results in the decoding of values outside the intended array bounds, potentially leading to bugs in contracts that use arrays within abidecode. The advisory has been assigned low severity, because it is only observable if there is a memory write between two invocations of abidecode on the same input.
Proof of Concept
vyper event Pwn: pass
@external def f(x: Bytes[32 3]): a: Bytes[32] = b"foo" y: Bytes[32 3] = x
decodedy1: Bytes[32] = abidecode(y, Bytes[32]) a = b"bar" decodedy2: Bytes[32] = abidecode(y, Bytes[32])
if decodedy1 != decodedy2: log Pwn()
Sending the following calldata results in Pwn being emitted.
0xd45754f8 0000000000000000000000000000000000000000000000000000000000000020 0000000000000000000000000000000000000000000000000000000000000060 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0
Patches Patched in https://github.com/vyperlang/vyper/pull/3925, https://github.com/vyperlang/vyper/pull/4091, https://github.com/vyperlang/vyper/pull/4144, https://github.com/vyperlang/vyper/pull/4060.
Summary Arrays can be keyed by a signed integer, while they are defined for unsigned integers only. The typechecker doesn't throw when spotting the usage of an int as an index for an array. Typically, negative integers are filtered out at runtime by the bounds checker, but small enough (i.e. large in magnitude, ex. -2255 + 5) quantities combined with large enough arrays (at least 2255 in length) can pass the bounds checker, resulting in unexpected behavior.
A contract search was performed, and no production contracts were found to be impacted.
Details The typechecker allows the usage of signed integers to be used as indexes to arrays. The vulnerability is present in different forms in all versions. Here is an example from 0.3.10: https://github.com/vyperlang/vyper/blob/c150fc49ee9375a930d177044559b83cb95f7963/vyper/semantics/types/subscriptable.py#L127-L137
As can be seen, the validation is performed against IntegerT.any().
PoC If the array is sufficiently large, it can be indexed with a negative value: python arr: public(uint256[MAXUINT256])
@external def set(idx: int256, num: uint256): self.arr[idx] = num For signed integers, the 2's complement representation is used. Because the array was declared very large, the bounds checking will pass (negative values will simply be represented as very large numbers): https://github.com/vyperlang/vyper/blob/a1fd228cb9936c3e4bbca6f3ee3fb4426ef45490/vyper/codegen/core.py#L534-L541
Patches Patched in https://github.com/vyperlang/vyper/pull/3817.
Impact There are two potential vulnerability classes: unpredictable behavior and accessing inaccessible elements.
1. If it is possible to index an array with a negative integer without reverting, this is most likely not anticipated by the developer and such accesses can cause unpredictable behavior for the contract.
2. If a contract has an invariant in the form assert index < x where both index and x are signed integers, the developer might suppose that no elements on indexes y | y >= x are accessible. However, by using negative indexes this can be bypassed.
The contract search found no production contracts impacted by these two classes of issues.
Summary There is an error in the stack management when compiling the IR for sha364. Concretely, the height variable is miscalculated. The vulnerability can't be triggered without writing the IR by hand. That is, it cannot be triggered from regular vyper code, it can only be triggered by using the fang binary directly (this binary used to be called vyper-ir prior to v0.3.4).
Details To compile sha364, the arg[0] and arg[1] have to be compiled: https://github.com/vyperlang/vyper/blob/c150fc49ee9375a930d177044559b83cb95f7963/vyper/ir/compileir.py#L585-L586
As can be seen, after compiling the 0th arg, the height variable isn't increased. If new withargs are defined in the inner scope, they are manipulated correctly, because both their height is off and also the global height is off and thus their placement on the stack is computed correctly.
sha364 is used for retrieval in mappings. No flow that would cache the key was found, the issue shouldn't be possible to trigger when compiling the compiler-generated IR.
PoC Suppose the following hand-written IR: lisp (with loc (with val 1 (with key 2 (sha364 val key))) (seq (sstore loc (with x (sload loc) (with ans (add x 1) (seq (assert (ge ans x)) ans)))))) after compilation: the generated bytecode: 6001600281806020525f5260405f2090509050805460018101818110610026579050815550005b5f80fd
0000 60 PUSH1 0x01 0002 60 PUSH1 0x02 0004 81 DUP2 0005 80 DUP1 bad code here!!!!!! 0006 60 PUSH1 0x20 0008 52 MSTORE
It can be seen that the second DUP will dup the item on the top of the stack which is incorrect.
Patches Patched in https://github.com/vyperlang/vyper/pull/4063.
Impact Versions v0.2.0-v0.3.10 were evaluated, and access of the variable with the invalid height is not reachable from IR generated by the vyper front-end. Because the issue isn't triggered during normal compilation of vyper code, the impact is considered low.
Summary
When calls to external contracts are made, we write the input buffer starting at byte 28, and allocate the return buffer to start at byte 0 (overlapping with the input buffer). When checking RETURNDATASIZE for dynamic types, the size is compared only to the minimum allowed size for that type, and not to the returned value's length. As a result, malformed return data can cause the contract to mistake data from the input buffer for returndata.
This advisory is given a severity of "Low" because when the called contract returns invalid ABIv2 encoded data, the calling contract can read different invalid data (from the dirty buffer) than the called contract returned.
Details
When arguments are packed for an external call, we create a buffer of size max(args, returndata) + 32. The input buffer is placed in this buffer (starting at byte 28), and the return buffer is allocated to start at byte 0. The assumption is that we can reuse the memory becase we will not be able to read past RETURNDATASIZE.
python if fntype.returntype is not None: returnabit = calculatetypeforexternalreturn(fntype.returntype).abitype
# we use the same buffer for args and returndata, # so allocate enough space here for the returndata too. buflen = max(argsabit.sizebound(), returnabit.sizebound()) else: buflen = argsabit.sizebound()
buflen += 32 # padding for the method id
When data is returned, we unpack the return data by starting at byte 0. We check that RETURNDATASIZE is greater than the minimum allowed for the returned type: python if not callkwargs.skipcontractcheck: assertion = IRnode.fromlist( ["assert", ["ge", "returndatasize", minreturnsize]], errormsg="returndatasize too small", ) unpacker.append(assertion)
This check ensures that any dynamic types returned will have a size of at least 64. However, it does not verify that RETURNDATASIZE is as large as the length word of the dynamic type.
As a result, if a contract expects a dynamic type to be returned, and the part of the return data that is read as length includes a size that is larger than the actual RETURNDATASIZE, the return data read from the buffer will overrun the actual return data size and read from the input buffer.
Proof of Concept
This contract calls an external contract with two arguments. As the call is made, the buffer includes: - byte 28: methodid - byte 32: first argument (0) - byte 64: second argument (hash)
The return data buffer begins at byte 0, and will return the returned bytestring, up to a maximum length of 96 bytes.
python interface Zero: def sneaky(a: uint256, b: bytes32) -> Bytes[96]: view
@external def testsneaky(z: address) -> Bytes[96]: return Zero(z).sneaky(0, keccak256("oops")) On the other side, imagine a simple contract that does not, in fact, return a bytestring, but instead returns two uint256s. I've implemented it in Solidity for ease of use with Foundry: solidity function sneaky(uint a, bytes32 b) external pure returns (uint, uint) { return (32, 32); }
The return data will be parsed as a bytestring. The first 32 will point us to byte 32 to read the length. The second 32 will be perceived as the length. It will then read the next 32 bytes from the return data buffer, even though those weren't a part of the return data.
Since these bytes will come from byte 64, we can see above that the hash was placed there in the input buffer.
If we run the following Foundry test, we can see that this does in fact happen: solidity function testsneakyZeroReturn() public { ZeroReturn z = new ZeroReturn(); c = SuperContract(deployer.deploy("src/loose/", "retoverflow", "")); console.logBytes(c.testsneaky(address(z))); }
md Logs: 0xd54c03ccbc84dd6002c98c6df5a828e42272fc54b512ca20694392ca89c4d2c6
Patches Patched in https://github.com/vyperlang/vyper/pull/3925, https://github.com/vyperlang/vyper/pull/4091, https://github.com/vyperlang/vyper/pull/4144, https://github.com/vyperlang/vyper/pull/4060.
Impact
Malicious or mistaken contracts returning the malformed data can result in overrunning the returned data and reading return data from the input buffer.
Summary
The bounds check for slices does not account for the ability for start + length to overflow when the values aren't literals.
If a slice() function uses a non-literal argument for the start or length variable, this creates the ability for an attacker to overflow the bounds check.
This issue can be used to do OOB access to storage, memory or calldata addresses. It can also be used to corrupt the length slot of the respective array.
A contract search was performed and no vulnerable contracts were found in production.
tracking in issue https://github.com/vyperlang/vyper/issues/3756. patched in https://github.com/vyperlang/vyper/pull/3818.
Details Here the flow for storage is supposed, but it is generalizable also for the other locations.
When calling slice() on a storage value, there are compile time bounds checks if the start and length values are literals, but of course this cannot happen if they are passed values:
python if not isadhocslice: if lengthliteral is not None: if lengthliteral < 1: raise ArgumentException("Length cannot be less than 1", lengthexpr)
if lengthliteral > argtype.length: raise ArgumentException(f"slice out of bounds for {argtype}", lengthexpr)
if startliteral is not None: if startliteral > argtype.length: raise ArgumentException(f"slice out of bounds for {argtype}", startexpr) if lengthliteral is not None and startliteral + lengthliteral > argtype.length: raise ArgumentException(f"slice out of bounds for {argtype}", node)
At runtime, we perform the following equivalent check, but the runtime check does not account for overflows: python ["assert", ["le", ["add", start, length], srclen]], # bounds check
The storage slice() function copies bytes directly from storage into memory and returns the memory value of the resulting slice. This means that, if a user is able to input the start or length value, they can force an overflow and access an unrelated storage slot.
In most cases, this will mean they have the ability to forcibly return 0 for the slice, even if this shouldn't be possible. In extreme cases, it will mean they can return another unrelated value from storage.
POC: OOB access
For simplicity, take the following Vyper contract, which takes an argument to determine where in a Bytes[64] bytestring should be sliced. It should only accept a value of zero, and should revert in all other cases.
python @version ^0.3.9
x: public(Bytes[64]) secret: uint256
@external def init(): self.x = empty(Bytes[64]) self.secret = 42
@external def sliceit(start: uint256) -> Bytes[64]: return slice(self.x, start, 64)
We can use the following manual storage to demonstrate the vulnerability: json {"x": {"type": "bytes32", "slot": 0}, "secret": {"type": "uint256", "slot": 3618502788666131106986593281521497120414687020801267626233049500247285301248}}
If we run the following test, passing max - 63 as the start value, we will overflow the bounds check, but access the storage slot at 1 + (2256 - 63) / 32, which is what was set in the above storage layout: solidity function testsliceerror() public { c = SuperContract(deployer.deploywithcustomstorage("src/loose/", "sliceerror", "sliceerrorstorage")); bytes memory result = c.sliceit(115792089237316195423570985008687907853269984665640564039457584007913129639872); // max - 63 console.logBytes(result); }
The result is that we return the secret value from storage: Logs: 0x0000...00002a POC: length corruption OOG exception doesn't have to be raised - because of the overflow, only a few bytes can be copied, but the length slot is set with the original input value.
python d: public(Bytes[256]) @external def test(): x : uint256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935 # 2256-1 self.d = b"\x01\x02\x03\x04\x05\x06" # s : Bytes[256] = slice(self.d, 1, x) assert len(slice(self.d, 1, x))==115792089237316195423570985008687907853269984665640564039457584007913129639935 The corruption of length can be then used to read dirty memory: python @external def test(): x: uint256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935 # 2256 - 1 y: uint256 = 22704331223003175573249212746801550559464702875615796870481879217237868556850 # 0x3232323232323232323232323232323232323232323232323232323232323232 z: uint96 = 1 if True: placeholder : uint256[16] = [y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y] s :String[32] = slice(uint2str(z), 1, x) # uint2str(z) == "1" #print(len(s)) assert slice(s, 1, 2) == "22"
Impact
The built-in slice() method can be used for OOB accesses or the corruption of the length slot.
Summary Vyper compiler allows passing a value in builtin rawcall even if the call is a delegatecall or a staticcall. But in the context of delegatecall and staticcall the handling of value is not possible due to the semantics of the respective opcodes, and vyper will silently ignore the value= argument.
A contract search was performed and no vulnerable contracts were found in production.
Details The IR for rawcall is built in the RawCall class: https://github.com/vyperlang/vyper/blob/9136169468f317a53b4e7448389aa315f90b95ba/vyper/builtins/functions.py#L1100
However, the compiler doesn't validate that if either delegatecall or staticall are provided as kwargs, that value wasn't set. For example, the following compiles without errors: python rawcall(self, calldata2, maxoutsize=255, isdelegatecall=True, value=msg.value/2)
Impact If the semantics of the EVM are unknown to the developer, he could suspect that by specifying the value kwarg, exactly the given amount will be sent along to the target. However in fact, no value will be sent.
Here is an example of an potentially problematic implementation of multicall utilizing the rawcall built-in: python valueaccumulator: uint256 = empty(uint256) results: DynArray[Result, maxvalue(uint8)] = [] returndata: Bytes[maxvalue(uint8)] = b"" success: bool = empty(bool) for batch in data: msgvalue: uint256 = batch.value valueaccumulator = unsafeadd(valueaccumulator, msgvalue) if (batch.allowfailure == False): returndata = rawcall(self, batch.calldata, maxoutsize=255, value=msgvalue, isdelegatecall=True) success = True results.append(Result({success: success, returndata: returndata})) else: success, returndata = \ rawcall(self, batch.calldata, maxoutsize=255, value=msgvalue, isdelegatecall=True, revertonfailure=False) results.append(Result({success: success, returndata: returndata})) assert msg.value == valueaccumulator, "Multicall: value mismatch" return results
Patches Fixed in https://github.com/vyperlang/vyper/pull/3755 Workarounds Is there a way for users to fix or remediate the vulnerability without upgrading?
References Are there any links users can visit to find out more?
Summary concat built-in can write over the bounds of the memory buffer that was allocated for it and thus overwrite existing valid data. The root cause is that the buildIR for concat doesn't properly adhere to the API of copy functions (for >=0.3.2 the copybytes function).
A contract search was performed and no vulnerable contracts were found in production.
Tracked in issue https://github.com/vyperlang/vyper/issues/3737
Details The buildIR allocates a new internal variable for the concatenation: https://github.com/vyperlang/vyper/blob/3b310d5292c4d1448e673d7b3adb223f9353260e/vyper/builtins/functions.py#L534-L550
Notice that the buffer is allocated for the maxlen + 1 word to actually hold the length of the array.
Later the copybytes function is used to copy the actual source arguments to the destination: https://github.com/vyperlang/vyper/blob/3b310d5292c4d1448e673d7b3adb223f9353260e/vyper/builtins/functions.py#L569-L572
The dstdata is defined via: - data ptr - to skip the 1 word that holds the length - offset - to skip the source arguments that were already written to the buffer - the offset is increased via: ["set", ofst, ["add", ofst, arglen]], ie it is increased by the length of the source argument
Now, the copybytes function has multiple control flow paths, the following ones are of interest: 1) https://github.com/vyperlang/vyper/blob/3b310d5292c4d1448e673d7b3adb223f9353260e/vyper/codegen/core.py#L270-L273 2) https://github.com/vyperlang/vyper/blob/3b310d5292c4d1448e673d7b3adb223f9353260e/vyper/codegen/core.py#L301-L320
Note that the function itself contains the following note: https://github.com/vyperlang/vyper/blob/3b310d5292c4d1448e673d7b3adb223f9353260e/vyper/codegen/core.py#L245-L247
That is we can ask for a copy of 1B yet a whole word is copied.
Consider the first interesting path - if the dstdata's distance to the end of the concat data buffer is < 32B, the copyop = STORE(dst, LOAD(src)) from copybytes will result in buffer overflow as it essentially will mstore to dstdata the mload of the source (mload will load whole word and the distance of the dstdata to the word boundary is <32B).
From the two mentioned paths in copybytes it can be seen that both sources from memory and storage can cause the corruption.
PoC The main attack vector that was found was when the concat is inside an internal function. Suppose we have an external function that calls internal one. In such case the address space is divided such that the memory for the internal function is in lower portion of the adr space. As such the buffer overflow can overwrite valid data of the caller.
Here is a simple example: python #@version ^0.3.9
@internal def bar() -> uint256: sss: String[2] = concat("a", "b") return 1
@external def foo() -> int256: a: int256 = -1 b: uint256 = self.bar() return a
foo should clearly return -1, but it returns 452312848583266388373324160190187140051835877600158453279131187530910662655
-1 was used intentionally due to its bit structure but the value here is fairly irelevant. In this example during the second iteration of the for loop in the buildIR mload to dst+1 will be executed (because len('a') == 1), thus the function will write 1B over the bounds of the buffer. The string 'b' is stored such that its right-most byte is a zero byte. So a zero byte will be written over the bounds. So when -1 is considered it's left-most B will be overwritten to all 0. Therefore it can be seen: 452312848583266388373324160190187140051835877600158453279131187530910662655 == (2248-1) will output True.
IR If we look at the contract's IR (vyper --no optimize -f ir), we see: Line 30 / a: int256 = -1 / [mstore, 320, -1 <-1>], And for the second iteration of the loop in concat: len, [mload, arg], [seq, [with, src, [add, arg, 32], [with, dst, [add, [add, 256 <concat destination>, 32], concatofst], [mstore, dst, [mload, src]]]], [set, concatofst, [add, concatofst, len]]]]], [mstore, 256 <concat destination>, concatofst], 256 <concat destination>]], So the address of the int is 320.
The dst is defined as: [add, [add, 256 <concat destination>, 32], concatofst],. In the second iteration the concatofst will be 1 because len('a)==1 so 256+32+1 = 289. Now this address will be mstored to - so the last mstored B will have the address 289+32=320 which clearly overlaps with the address of the int a.
PoC 2 Due to how immutables are handled, they can be corrupted too: python #@version ^0.3.9
i: immutable(int256)
@external def init(): i = -1 s: String[2] = concat("a", "b")
@external def foo() -> int256: return i
Output of calling foo() = 452312848583266388373324160190187140051835877600158453279131187530910662655.
Impact The buffer overflow can result in the change of semantics of the contract. The overflow is length-dependent and thus it might go unnoticed during contract testing.
However, certainly not all usages of concat will result in overwritten valid data as we require it to be in an internal function and close to the return statement where other memory allocations don't occur.
Concluding remarks The bug based on the fast path in copybytes was likely introduced in: 548d35d720fb6fd8efbdc0ce525bed259a73f0b9. git bisect was used between v0.3.1 and v0.3.2, forge test was run and the test asserted that the function indeed returns -1.
For the general case, 0.3.0 and 0.3.1 are also affected.
Impact contracts containing large arrays might underallocate the number of slots they need. prior to v0.3.8, the calculation to determine how many slots a storage variable needed used math.ceil(type.sizeinbytes / 32):
https://github.com/vyperlang/vyper/blob/6020b8bbf66b062d299d87bc7e4eddc4c9d1c157/vyper/semantics/validation/datapositions.py#L197
the intermediate floating point step can produce a rounding error if there are enough bits set in the IEEE-754 mantissa. roughly speaking, if type.sizeinbytes is large (> 246), and slightly less than a power of 2, the calculation can overestimate how many slots are needed. if type.sizeinbytes is slightly more than a power of 2, the calculation can underestimate how many slots are needed.
the following two example contracts can result in overwriting of the variable vulnerable: vyper largearray: address[264 + 1] # type.sizeinbytes == 32 (264 + 1); math.ceil(type.sizeinbytes / 32) < 264 + 1 vulnerable: uint256
writing to self.largearray[264] will overwrite self.vulnerable vyper largedynarray: DynArray[address, 264] # Dynarray has a length word in front, its size in bytes is 32 (264 + 1) vulnerable: uint256
writing to self.largedynarray[264 - 1] will overwrite self.vulnerable
note that in the latter case, the risk of vulnerable being overwritten is relatively small, since it would cost roughly $1.45 million trillion USD at today's gas prices (gas price 20gwei, ETH ~= $1800) in order to extend the DynArray to its full container size.
Patches patched by v0.3.8, specifically in commit https://github.com/vyperlang/vyper/commit/0bb7203b584e771b23536ba065a6efda457161bb.
Impact abidecode() does not validate input when it is nested in an expression. the following example gets correctly validated (bounds checked): vyper x: int128 = abidecode(slice(msg.data, 4, 32), int128)
however, the following example is not bounds checked vyper @external def abidecode(x: uint256) -> uint256: a: uint256 = convert(abidecode(slice(msg.data, 4, 32), (uint8)), uint256) + 1 return a # abidecode(256) returns: 257
the issue can be triggered by constructing an example where the output of abidecode is not internally passed to makesetter (an internal codegen routine) or other input validating routine.
Patches https://github.com/vyperlang/vyper/pull/3626
Workarounds Is there a way for users to fix or remediate the vulnerability without upgrading?
References Are there any links users can visit to find out more?
Impact In certain conditions, the memory used by the builtins rawcall, createfromblueprint and createcopyof can be corrupted.
- For rawcall, the argument buffer of the call can be corrupted, leading to incorrect calldata in the sub-context. - For createfromblueprint and createcopyof, the buffer for the to-be-deployed bytecode can be corrupted, leading to deploying incorrect bytecode.
Below are the conditions that must be fulfilled for the corruption to happen for each builtin:
rawcall - memory is not fully initialized, ex. all parameters to an external function live in calldata and - The data argument of the builtin is msg.data. and - The to, value or gas passed to the builtin is some complex expression that results in writing to uninitialized memory (e.g. calling an internal function)
createcopyof - memory is not fully initialized, ex. all parameters to an external function live in calldata and - The value or salt passed to the builtin is some complex expression that results in writing to uninitialized memory (e.g. calling an internal function)
createfromblueprint - memory is not fully initialized, ex. all parameters to an external function live in calldata and - Either no constructor parameters are passed to the builtin or rawargs is set to True. and - The value or salt passed to the builtin is some complex expression that results in writing to uninitialized memory (e.g. calling an internal function)
Note: When the builtin is being called from an internal function f from a function g, the issue is not present provided that g has written to memory before calling f. Examples
rawcall
In the following contract, calling bar(1,1) will return:
Python ae42e95100000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000001 instead of: Python ae42e95100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001
Python identity: constant(address) = 0x0000000000000000000000000000000000000004
@external def foo(): pass
@internal @view def getaddress()->address: a:uint256 = maxvalue(uint256) # 0xfff...fff return identity @external def bar(f:uint256, u:uint256) -> Bytes[100]: a: Bytes[100] = rawcall(self.getaddress(), msg.data, maxoutsize=100) return a
createcopyof In the following contract, after calling test(), the code deployed at self.createdaddress does not match the bytecode at target.
Vyper createdaddress: public(address)
@external def test(target: address) -> address: # The expression in salt= is complex and will require to store to memory self.createdaddress = createcopyof(target, salt = keccak256(abiencode(target))) return self.createdaddress
createfromblueprint In the following contract, after calling test(), the init bytecode used to create the contract deployed at the address self.createdaddress will not match the blueprint bytecode stored at target.
Vyper createdaddress: public(address)
salt: constant(bytes32) = keccak256("kebab")
@external @payable def test(target: address): # The expression in salt= is complex and will require to store to memory self.createdaddress = createfromblueprint(target, codeoffset=0, salt=keccak256(abiencode(target))) Patches issue tracking in https://github.com/vyperlang/vyper/issues/3609, patched in #3610
Workarounds
The complex expressions that are being passed as kwargs to the builtin should be cached in memory prior to the call to the builtin. For the last example above, it would be:
Vyper createdaddress: public(address)
salt: constant(bytes32) = keccak256("kebab")
@external @payable def test(target: address): salt: bytes32 = keccak256(abiencode(target)) self.createdaddress = createfromblueprint(target, codeoffset=0, salt=salt) References Are there any links users can visit to find out more?
Impact
Locks of the type @nonreentrant("") or @nonreentrant('') do not produce reentrancy checks at runtime.
Vyper @nonreentrant("") # unprotected @external def bar(): pass
@nonreentrant("lock") # protected @external def foo(): pass Patches
Patched in #3605
Workarounds
The lock name should be a non-empty string.
References Are there any links users can visit to find out more?
Impact The order of evaluation of the arguments of the builtin functions uint256addmod, uint256mulmod, ecadd and ecmul does not follow source order. • For uint256addmod(a,b,c) and uint256mulmod(a,b,c), the order is c,a,b. • For ecadd(a,b) and ecmul(a,b), the order is b,a.
Note that this behaviour is problematic when the evaluation of one of the arguments produces side effects that other arguments depend on.
Patches https://github.com/vyperlang/vyper/pull/3583
Workarounds When using builtins from the list above, make sure that the arguments of the expression do not produce side effects or, if one does, that no other argument is dependent on those side effects.
References Are there any links users can visit to find out more?
Impact
For the following (probably non-exhaustive) list of expressions, the compiler evaluates the arguments from right to left instead of left to right.
- unsafeadd - unsafesub - unsafemul - unsafediv - powmod256 - |, &, ^ (bitwise operators) - bitwiseor (deprecated) - bitwiseand (deprecated) - bitwisexor (deprecated) - rawcall - <, >, <=, >=, ==, != - in, not in (when lhs and rhs are enums)
This behaviour becomes a problem when the evaluation of one of the arguments produces side effects that other arguments depend on. The following expressions can produce side-effect:
- state modifying external call - state modifying internal call - rawcall - pop() when used on a Dynamic Array stored in the storage - createminimalproxyto - createcopyof - createfromblueprint
For example:
Vyper f:uint256
@internal def sideeffect() -> uint256: self.f = 12 return 1
@external def foo() -> uint256: return unsafeadd(self.f,self.sideeffect()) # returns 13 instead of 1
Vyper a:DynArray[uint256, 12] @external def bar() -> bool: self.a = [1,2,3] return len(self.a) == self.a.pop() # return false instead of true
Patches not yet patched, will address in a future release. tracking in https://github.com/vyperlang/vyper/issues/3604.
Workarounds
When using expressions from the list above, make sure that the arguments of the expression do not produce side effects or, if one does, that no other argument is dependent on those side effects.
References Are there any links users can visit to find out more?
Impact
In versions 0.2.15, 0.2.16 and 0.3.0, named re-entrancy locks are allocated incorrectly. Each function using a named re-entrancy lock gets a unique lock regardless of the key, allowing cross-function re-entrancy in contracts compiled with the susceptible versions. A specific set of conditions is required to result in misbehavior of affected contracts, specifically:
- A .vy contract compiled with either of the following vyper versions: 0.2.15, 0.2.16, 0.3.0 - A primary function that utilizes the @nonreentrant decorator with a specific key and does not strictly follow the check-effects-interaction pattern (i.e. contains an external call to an untrusted party before storage updates) - A secondary function that utilizes the same key and would be affected by the improper state caused by the primary function
Patches https://github.com/vyperlang/vyper/pull/2439, https://github.com/vyperlang/vyper/pull/2514
Workarounds Upgrade to 0.3.1 or higher
References Technical post-mortem report: https://hackmd.io/@vyperlang/HJUgNMhs2
Impact the ecrecover precompile does not fill the output buffer if the signature does not verify, see https://github.com/ethereum/go-ethereum/blob/b058cf454b3bdc7e770e2b3cec83a0bcb48f55ee/core/vm/contracts.go#L188. however, the ecrecover builtin will still return whatever is at memory location 0.
this means that the if the compiler has been convinced to write to the 0 memory location with specially crafted data (generally, this can happen with a hashmap access or immutable read) just before the ecrecover, a signature check might pass on an invalid signature.
A contract search was performed. Most uses of ecrecover are used for erc2612-style permit implementations, which typically look like:
vyper assert owner != empty(address) assert block.timestamp <= deadline nonce: uint256 = self.nonces[owner] digest: bytes32 = keccak256( concat( b"\x19\x01", self.DOMAINSEPARATOR, keccak256(abiencode(PERMITTYPEHASH, owner, spender, value, nonce, deadline)) ) ) assert ecrecover(digest, convert(v, uint256), convert(r, uint256), convert(s, uint256)) == owner
in this case, the immutable PERMITTYPEHASH is loaded into ecrecover's output buffer right before ecrecover(), and so the output of ecrecover() here when the signature is invalid will be the value of PERMITTYPEHASH. in this case, since PERMITTYPEHASH is not a valid address, it will never compare == to owner, and so the behaviour is exactly the same as if ecrecover() returned 0 in this case.
in general, a contract could have unexpected behavior (i.e. mistakenly pass this style of signature check) if an immutable representing a real address (ex. OWNER) was read right before the ecrecover operation.
Patches v0.3.10 (with 019a37ab98ff53f04fecfadf602b6cd5ac748f7f and #3586)
Workarounds Is there a way for users to fix or remediate the vulnerability without upgrading?
References Are there any links users can visit to find out more?
Impact in contracts with at least one regular nonpayable function, due to the callvalue check being inside of the selector section, it is possible to send funds to the default function by using less than 4 bytes of calldata, even if the default function is marked nonpayable. this applies to contracts compiled with vyper<=0.3.7. vyper @version 0.3.7
implicitly nonpayable @external def foo() -> uint256: return 1
implicitly nonpayable @external def default(): # could receive ether here pass
Patches this was fixed by the removal of the global calldatasize check in https://github.com/vyperlang/vyper/commit/02339dfda0f3caabad142060d511d10bfe93c520.
Workarounds don't use nonpayable default functions
Impact
Internal calls to internal functions with more than 1 default argument are compiled incorrectly. Depending on the number of arguments provided in the call, the defaults are added not right-to-left, but left-to-right. If the types are incompatible, typechecking is bypassed. In the bar() function in the following code, self.foo(13) is compiled to self.foo(13,12) instead of self.foo(13,1337).
vyper @internal def foo(a:uint256 = 12, b:uint256 = 1337): pass
@internal def bar(): self.foo(13)
note that at the time of publication, the ability to pass kwargs to internal functions is an undocumented feature that does not seem to be widely used.
Patches patched in c3e68c302aa6e1429946473769dd1232145822ac
Workarounds Is there a way for users to fix or remediate the vulnerability without upgrading?
References Are there any links users can visit to find out more?
Impact
Due to missing overflow check for loop variables, by assigning the iterator of a loop to a variable, it is possible to overflow the type of the latter.
In the following example, calling test returns 354, meaning that the variable a did store 354 a value out of bound for the type uint8.
Vyper @external def test() -> uint16: x:uint8 = 255 a:uint8 = 0 for i in range(x, x+100): a = i return convert(a,uint16)
The issue seems to happen only in loops of type for i in range(a, a + N) as in loops of type for i in range(start, stop) and for i in range(stop), the compiler is able to raise a TypeMismatch when trying to overflow the variable.
thanks to @trocher for reporting
Patches
patched in 3de1415ee77a9244eb04bdb695e249d3ec9ed868
Workarounds
Impact during codegen, the length word of a dynarray is written before the data, which can result in OOB array access in the case where the dynarray is on both the lhs and rhs of an assignment. here is a minimal example producing the issue: vyper a:DynArray[uint256,3] @external def test() -> DynArray[uint256,3]: self.a = [1,2,3] self.a = empty(DynArray[uint256,3]) self.a = [self.a[0],self.a[1],self.a[2]] return self.a # return [1,2,3]
and here is an example demonstrating the issue can cause data corruption across call frames:
vyper @external def test() -> DynArray[uint256,3]: self.a() return self.b() # return [1,2,3]
@internal def a(): a: uint256 = 0 b: uint256 = 1 c: uint256 = 2 d: uint256 = 3
@internal def b() -> DynArray[uint256,3]: a: DynArray[uint256,3] = empty(DynArray[uint256,3]) a = [a[0],a[1],a[2]] return a
examples involving append and pop: vyper @internal def foo(): c: DynArray[uint256, 1] = [] c.append(c[0])
vyper @internal def foo(): c: DynArray[uint256, 1] = [1] c[0] = c.pop()
the expected behavior in all of the above cases is to revert due to oob array access.
Patches patched in 4f8289a81206f767df1900ac48f485d90fc87edb
Workarounds Is there a way for users to fix or remediate the vulnerability without upgrading?
References Are there any links users can visit to find out more?