Skip to content

Commit

Permalink
Merge branch 'master' into fix/abi_decode_validation
Browse files Browse the repository at this point in the history
  • Loading branch information
charles-cooper committed Sep 26, 2023
2 parents de8666b + d438d92 commit 4ced757
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 45 deletions.
13 changes: 9 additions & 4 deletions docs/built-in-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,14 @@ Vyper has three built-ins for contract creation; all three contract creation bui
The implementation of ``create_copy_of`` assumes that the code at ``target`` is smaller than 16MB. While this is much larger than the EIP-170 constraint of 24KB, it is a conservative size limit intended to future-proof deployer contracts in case the EIP-170 constraint is lifted. If the code at ``target`` is larger than 16MB, the behavior of ``create_copy_of`` is undefined.


.. py:function:: create_from_blueprint(target: address, *args, value: uint256 = 0, code_offset=0, [, salt: bytes32]) -> address
.. py:function:: create_from_blueprint(target: address, *args, value: uint256 = 0, raw_args: bool = False, code_offset: int = 0, [, salt: bytes32]) -> address
Copy the code of ``target`` into memory and execute it as initcode. In other words, this operation interprets the code at ``target`` not as regular runtime code, but directly as initcode. The ``*args`` are interpreted as constructor arguments, and are ABI-encoded and included when executing the initcode.

* ``target``: Address of the blueprint to invoke
* ``*args``: Constructor arguments to forward to the initcode.
* ``value``: The wei value to send to the new contract address (Optional, default 0)
* ``raw_args``: If ``True``, ``*args`` must be a single ``Bytes[...]`` argument, which will be interpreted as a raw bytes buffer to forward to the create operation (which is useful for instance, if pre- ABI-encoded data is passed in from elsewhere). (Optional, default ``False``)
* ``code_offset``: The offset to start the ``EXTCODECOPY`` from (Optional, default 0)
* ``salt``: A ``bytes32`` value utilized by the deterministic ``CREATE2`` opcode (Optional, if not supplied, ``CREATE`` is used)

Expand All @@ -201,7 +202,7 @@ Vyper has three built-ins for contract creation; all three contract creation bui
@external
def foo(blueprint: address) -> address:
arg1: uint256 = 18
arg2: String = "some string"
arg2: String[32] = "some string"
return create_from_blueprint(blueprint, arg1, arg2, code_offset=1)
.. note::
Expand All @@ -226,7 +227,7 @@ Vyper has three built-ins for contract creation; all three contract creation bui
* ``to``: Destination address to call to
* ``data``: Data to send to the destination address
* ``max_outsize``: Maximum length of the bytes array returned from the call. If the returned call data exceeds this length, only this number of bytes is returned. (Optional, default ``0``)
* ``gas``: The amount of gas to attach to the call. If not set, all remaining gas is forwarded.
* ``gas``: The amount of gas to attach to the call. (Optional, defaults to ``msg.gas``).
* ``value``: The wei value to send to the address (Optional, default ``0``)
* ``is_delegate_call``: If ``True``, the call will be sent as ``DELEGATECALL`` (Optional, default ``False``)
* ``is_static_call``: If ``True``, the call will be sent as ``STATICCALL`` (Optional, default ``False``)
Expand Down Expand Up @@ -264,6 +265,10 @@ Vyper has three built-ins for contract creation; all three contract creation bui
assert success
return response
.. note::

Regarding "forwarding all gas", note that, while Vyper will provide ``msg.gas`` to the call, in practice, there are some subtleties around forwarding all remaining gas on the EVM which are out of scope of this documentation and could be subject to change. For instance, see the language in EIP-150 around "all but one 64th".

.. py:function:: raw_log(topics: bytes32[4], data: Union[Bytes, bytes32]) -> None
Provides low level access to the ``LOG`` opcodes, emitting a log without having to specify an ABI type.
Expand Down Expand Up @@ -500,7 +505,7 @@ Data Manipulation

* ``b``: ``Bytes`` list to extract from
* ``start``: Start point to extract from
* ``output_type``: Type of output (``bytes32``, ``integer``, or ``address``). Defaults to ``bytes32``.
* ``output_type``: Type of output (``bytesM``, ``integer``, or ``address``). Defaults to ``bytes32``.

Returns a value of the type specified by ``output_type``.

Expand Down
1 change: 1 addition & 0 deletions docs/compiling-a-contract.rst
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ The following is a list of supported EVM versions, and changes in the compiler i
- The ``transient`` keyword allows declaration of variables which live in transient storage
- Functions marked with ``@nonreentrant`` are protected with TLOAD/TSTORE instead of SLOAD/SSTORE
- The ``MCOPY`` opcode will be generated automatically by the compiler for most memory operations.



Expand Down
28 changes: 28 additions & 0 deletions tests/parser/functions/test_abi_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,34 @@ def abi_decode(x: Bytes[96]) -> (uint256, uint256):
assert_tx_failed(lambda: c.abi_decode(input_))


def test_clamper_nested_uint8(get_contract, assert_tx_failed):
# check that _abi_decode clamps on word-types even when it is in a nested expression
# decode -> validate uint8 -> revert if input >= 256 -> cast back to uint256
contract = """
@external
def abi_decode(x: uint256) -> uint256:
a: uint256 = convert(_abi_decode(slice(msg.data, 4, 32), (uint8)), uint256)
return a
"""
c = get_contract(contract)
assert c.abi_decode(255) == 255
assert_tx_failed(lambda: c.abi_decode(256))


def test_clamper_nested_bytes(get_contract, assert_tx_failed):
# check that _abi_decode clamps dynamic even when it is in a nested expression
# decode -> validate Bytes[20] -> revert if len(input) > 20 -> convert back to -> add 1
contract = """
@external
def abi_decode(x: Bytes[96]) -> Bytes[21]:
a: Bytes[21] = concat(b"a", _abi_decode(x, Bytes[20]))
return a
"""
c = get_contract(contract)
assert c.abi_decode(abi.encode("(bytes)", (b"bc",))) == b"abc"
assert_tx_failed(lambda: c.abi_decode(abi.encode("(bytes)", (b"a" * 22,))))


@pytest.mark.parametrize(
"output_typ,input_",
[
Expand Down
58 changes: 26 additions & 32 deletions vyper/builtins/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
get_type_for_exact_size,
ir_tuple_from_args,
make_setter,
needs_external_call_wrap,
promote_signed_int,
sar,
shl,
Expand Down Expand Up @@ -1418,7 +1417,7 @@ class BitwiseNot(BuiltinFunction):

def evaluate(self, node):
if not self.__class__._warned:
vyper_warn("`bitwise_not()` is deprecated! Please use the ^ operator instead.")
vyper_warn("`bitwise_not()` is deprecated! Please use the ~ operator instead.")
self.__class__._warned = True

validate_call_args(node, 1)
Expand Down Expand Up @@ -1917,9 +1916,8 @@ def _build_create_IR(self, expr, args, context, value, salt, code_offset, raw_ar

# copy the target code into memory.
# layout starting from mem_ofst:
# 00...00 (22 0's) | preamble | bytecode
# <target initcode> | <abi-encoded args OR arg buffer if raw_arg=True>
ir.append(["extcodecopy", target, mem_ofst, code_offset, codesize])

ir.append(copy_bytes(add_ofst(mem_ofst, codesize), argbuf, encoded_args_len, bufsz))

# theoretically, dst = "msize", but just be safe.
Expand Down Expand Up @@ -2368,8 +2366,6 @@ def build_IR(self, expr, args, kwargs, context):
class ABIEncode(BuiltinFunction):
_id = "_abi_encode" # TODO prettier to rename this to abi.encode
# signature: *, ensure_tuple=<literal_bool> -> Bytes[<calculated len>]
# (check the signature manually since we have no utility methods
# to handle varargs.)
# explanation of ensure_tuple:
# default is to force even a single value into a tuple,
# e.g. _abi_encode(bytes) -> _abi_encode((bytes,))
Expand Down Expand Up @@ -2532,24 +2528,11 @@ def build_IR(self, expr, args, kwargs, context):
)

data = ensure_in_memory(data, context)

with data.cache_when_complex("to_decode") as (b1, data):
data_ptr = bytes_data_ptr(data)
data_len = get_bytearray_length(data)

# Normally, ABI-encoded data assumes the argument is a tuple
# (See comments for `wrap_value_for_external_return`)
# However, we do not want to use `wrap_value_for_external_return`
# technique as used in external call codegen because in order to be
# type-safe we would need an extra memory copy. To avoid a copy,
# we manually add the ABI-dynamic offset so that it is
# re-interpreted in-place.
if (
unwrap_tuple is True
and needs_external_call_wrap(output_typ)
and output_typ.abi_type.is_dynamic()
):
data_ptr = add_ofst(data_ptr, 32)

ret = ["seq"]

if abi_min_size == abi_size_bound:
Expand All @@ -2558,18 +2541,30 @@ def build_IR(self, expr, args, kwargs, context):
# runtime assert: abi_min_size <= data_len <= abi_size_bound
ret.append(clamp2(abi_min_size, data_len, abi_size_bound, signed=False))

# return pointer to the buffer
ret.append(data_ptr)

return b1.resolve(
IRnode.from_list(
ret,
typ=output_typ,
location=data.location,
encoding=Encoding.ABI,
annotation=f"abi_decode({output_typ})",
)
to_decode = IRnode.from_list(
data_ptr,
typ=wrapped_typ,
location=data.location,
encoding=Encoding.ABI,
annotation=f"abi_decode({output_typ})",
)
to_decode.encoding = Encoding.ABI

# TODO optimization: skip make_setter when we don't need
# input validation

output_buf = context.new_internal_variable(wrapped_typ)
output = IRnode.from_list(output_buf, typ=wrapped_typ, location=MEMORY)

# sanity check buffer size for wrapped output type will not buffer overflow
assert wrapped_typ.memory_bytes_required == output_typ.memory_bytes_required
ret.append(make_setter(output, to_decode))

ret.append(output)
# finalize. set the type and location for the return buffer.
# (note: unwraps the tuple type if necessary)
ret = IRnode.from_list(ret, typ=output_typ, location=MEMORY)
return b1.resolve(ret)


class _MinMaxValue(TypenameFoldedFunction):
Expand All @@ -2588,7 +2583,6 @@ def evaluate(self, node):
if isinstance(input_type, IntegerT):
ret = vy_ast.Int.from_node(node, value=val)

# TODO: to change to known_type once #3213 is merged
ret._metadata["type"] = input_type
return ret

Expand Down
11 changes: 4 additions & 7 deletions vyper/codegen/function_definitions/external_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,20 +135,17 @@ def handler_for(calldata_kwargs, default_kwargs):
return ret


# TODO it would be nice if this returned a data structure which were
# amenable to generating a jump table instead of the linear search for
# method_id we have now.
def generate_ir_for_external_function(code, func_t, context):
# TODO type hints:
# def generate_ir_for_external_function(
# code: vy_ast.FunctionDef,
# func_t: ContractFunctionT,
# context: Context,
# check_nonpayable: bool,
# ) -> IRnode:
"""Return the IR for an external function. Includes code to inspect the method_id,
enter the function (nonpayable and reentrancy checks), handle kwargs and exit
the function (clean up reentrancy storage variables)
"""
Return the IR for an external function. Returns IR for the body
of the function, handle kwargs and exit the function. Also returns
metadata required for `module.py` to construct the selector table.
"""
nonreentrant_pre, nonreentrant_post = get_nonreentrant_lock(func_t)

Expand Down
2 changes: 1 addition & 1 deletion vyper/semantics/analysis/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def _validate_msg_data_attribute(node: vy_ast.Attribute) -> None:
allowed_builtins = ("slice", "len", "raw_call")
if not isinstance(parent, vy_ast.Call) or parent.get("func.id") not in allowed_builtins:
raise StructureException(
"msg.data is only allowed inside of the slice or len functions", node
"msg.data is only allowed inside of the slice, len or raw_call functions", node
)
if parent.get("func.id") == "slice":
ok_args = len(parent.args) == 3 and isinstance(parent.args[2], vy_ast.Int)
Expand Down
1 change: 0 additions & 1 deletion vyper/semantics/analysis/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ def __init__(
_ns.update({k: namespace[k] for k in namespace._scopes[-1]}) # type: ignore
module_node._metadata["namespace"] = _ns

# check for collisions between 4byte function selectors
self_members = namespace["self"].typ.members

# get list of internal function calls made by each function
Expand Down

0 comments on commit 4ced757

Please sign in to comment.