mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
commit
07591478dd
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Features:
|
Features:
|
||||||
* Allow constant variables to be used as array length
|
* Allow constant variables to be used as array length
|
||||||
|
* Code Generator: New ABI decoder which supports structs and arbitrarily nested
|
||||||
|
arrays and checks input size (activate using ``pragma experimental ABIEncoderV2;``.
|
||||||
* Syntax Checker: Turn the usage of ``callcode`` into an error as experimental 0.5.0 feature.
|
* Syntax Checker: Turn the usage of ``callcode`` into an error as experimental 0.5.0 feature.
|
||||||
* Type Checker: Improve address checksum warning.
|
* Type Checker: Improve address checksum warning.
|
||||||
* Type Checker: More detailed errors for invalid array lengths (such as division by zero).
|
* Type Checker: More detailed errors for invalid array lengths (such as division by zero).
|
||||||
|
@ -22,9 +22,13 @@
|
|||||||
|
|
||||||
#include <libsolidity/codegen/ABIFunctions.h>
|
#include <libsolidity/codegen/ABIFunctions.h>
|
||||||
|
|
||||||
|
#include <libsolidity/ast/AST.h>
|
||||||
|
#include <libsolidity/codegen/CompilerUtils.h>
|
||||||
|
|
||||||
#include <libdevcore/Whiskers.h>
|
#include <libdevcore/Whiskers.h>
|
||||||
|
|
||||||
#include <libsolidity/ast/AST.h>
|
#include <boost/algorithm/string/join.hpp>
|
||||||
|
#include <boost/range/adaptor/reversed.hpp>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace dev;
|
using namespace dev;
|
||||||
@ -99,6 +103,79 @@ string ABIFunctions::tupleEncoder(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::tupleDecoder(TypePointers const& _types, bool _fromMemory)
|
||||||
|
{
|
||||||
|
string functionName = string("abi_decode_tuple_");
|
||||||
|
for (auto const& t: _types)
|
||||||
|
functionName += t->identifier();
|
||||||
|
if (_fromMemory)
|
||||||
|
functionName += "_fromMemory";
|
||||||
|
|
||||||
|
solAssert(!_types.empty(), "");
|
||||||
|
|
||||||
|
return createFunction(functionName, [&]() {
|
||||||
|
TypePointers decodingTypes;
|
||||||
|
for (auto const& t: _types)
|
||||||
|
decodingTypes.emplace_back(t->decodingType());
|
||||||
|
|
||||||
|
Whiskers templ(R"(
|
||||||
|
function <functionName>(headStart, dataEnd) -> <valueReturnParams> {
|
||||||
|
switch slt(sub(dataEnd, headStart), <minimumSize>) case 1 { revert(0, 0) }
|
||||||
|
<decodeElements>
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
templ("functionName", functionName);
|
||||||
|
templ("minimumSize", to_string(headSize(decodingTypes)));
|
||||||
|
|
||||||
|
string decodeElements;
|
||||||
|
vector<string> valueReturnParams;
|
||||||
|
size_t headPos = 0;
|
||||||
|
size_t stackPos = 0;
|
||||||
|
for (size_t i = 0; i < _types.size(); ++i)
|
||||||
|
{
|
||||||
|
solAssert(_types[i], "");
|
||||||
|
solAssert(decodingTypes[i], "");
|
||||||
|
size_t sizeOnStack = _types[i]->sizeOnStack();
|
||||||
|
solAssert(sizeOnStack == decodingTypes[i]->sizeOnStack(), "");
|
||||||
|
solAssert(sizeOnStack > 0, "");
|
||||||
|
vector<string> valueNamesLocal;
|
||||||
|
for (size_t j = 0; j < sizeOnStack; j++)
|
||||||
|
{
|
||||||
|
valueNamesLocal.push_back("value" + to_string(stackPos));
|
||||||
|
valueReturnParams.push_back("value" + to_string(stackPos));
|
||||||
|
stackPos++;
|
||||||
|
}
|
||||||
|
bool dynamic = decodingTypes[i]->isDynamicallyEncoded();
|
||||||
|
Whiskers elementTempl(
|
||||||
|
dynamic ?
|
||||||
|
R"(
|
||||||
|
{
|
||||||
|
let offset := <load>(add(headStart, <pos>))
|
||||||
|
switch gt(offset, 0xffffffffffffffff) case 1 { revert(0, 0) }
|
||||||
|
<values> := <abiDecode>(add(headStart, offset), dataEnd)
|
||||||
|
}
|
||||||
|
)" :
|
||||||
|
R"(
|
||||||
|
{
|
||||||
|
let offset := <pos>
|
||||||
|
<values> := <abiDecode>(add(headStart, offset), dataEnd)
|
||||||
|
}
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
elementTempl("load", _fromMemory ? "mload" : "calldataload");
|
||||||
|
elementTempl("values", boost::algorithm::join(valueNamesLocal, ", "));
|
||||||
|
elementTempl("pos", to_string(headPos));
|
||||||
|
elementTempl("abiDecode", abiDecodingFunction(*_types[i], _fromMemory, true));
|
||||||
|
decodeElements += elementTempl.render();
|
||||||
|
headPos += dynamic ? 0x20 : decodingTypes[i]->calldataEncodedSize();
|
||||||
|
}
|
||||||
|
templ("valueReturnParams", boost::algorithm::join(valueReturnParams, ", "));
|
||||||
|
templ("decodeElements", decodeElements);
|
||||||
|
|
||||||
|
return templ.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
string ABIFunctions::requestedFunctions()
|
string ABIFunctions::requestedFunctions()
|
||||||
{
|
{
|
||||||
string result;
|
string result;
|
||||||
@ -141,10 +218,9 @@ string ABIFunctions::cleanupFunction(Type const& _type, bool _revertOnFailure)
|
|||||||
solUnimplemented("Fixed point types not implemented.");
|
solUnimplemented("Fixed point types not implemented.");
|
||||||
break;
|
break;
|
||||||
case Type::Category::Array:
|
case Type::Category::Array:
|
||||||
solAssert(false, "Array cleanup requested.");
|
|
||||||
break;
|
|
||||||
case Type::Category::Struct:
|
case Type::Category::Struct:
|
||||||
solAssert(false, "Struct cleanup requested.");
|
solAssert(_type.dataStoredIn(DataLocation::Storage), "Cleanup requested for non-storage reference type.");
|
||||||
|
templ("body", "cleaned := value");
|
||||||
break;
|
break;
|
||||||
case Type::Category::FixedBytes:
|
case Type::Category::FixedBytes:
|
||||||
{
|
{
|
||||||
@ -367,6 +443,24 @@ string ABIFunctions::combineExternalFunctionIdFunction()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::splitExternalFunctionIdFunction()
|
||||||
|
{
|
||||||
|
string functionName = "split_external_function_id";
|
||||||
|
return createFunction(functionName, [&]() {
|
||||||
|
return Whiskers(R"(
|
||||||
|
function <functionName>(combined) -> addr, selector {
|
||||||
|
combined := <shr64>(combined)
|
||||||
|
selector := and(combined, 0xffffffff)
|
||||||
|
addr := <shr32>(combined)
|
||||||
|
}
|
||||||
|
)")
|
||||||
|
("functionName", functionName)
|
||||||
|
("shr32", shiftRightFunction(32, false))
|
||||||
|
("shr64", shiftRightFunction(64, false))
|
||||||
|
.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
string ABIFunctions::abiEncodingFunction(
|
string ABIFunctions::abiEncodingFunction(
|
||||||
Type const& _from,
|
Type const& _from,
|
||||||
Type const& _to,
|
Type const& _to,
|
||||||
@ -963,6 +1057,307 @@ string ABIFunctions::abiEncodingFunctionFunctionType(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::abiDecodingFunction(Type const& _type, bool _fromMemory, bool _forUseOnStack)
|
||||||
|
{
|
||||||
|
// The decoding function has to perform bounds checks unless it decodes a value type.
|
||||||
|
// Conversely, bounds checks have to be performed before the decoding function
|
||||||
|
// of a value type is called.
|
||||||
|
|
||||||
|
TypePointer decodingType = _type.decodingType();
|
||||||
|
solAssert(decodingType, "");
|
||||||
|
|
||||||
|
if (auto arrayType = dynamic_cast<ArrayType const*>(decodingType.get()))
|
||||||
|
{
|
||||||
|
if (arrayType->dataStoredIn(DataLocation::CallData))
|
||||||
|
{
|
||||||
|
solAssert(!_fromMemory, "");
|
||||||
|
return abiDecodingFunctionCalldataArray(*arrayType);
|
||||||
|
}
|
||||||
|
else if (arrayType->isByteArray())
|
||||||
|
return abiDecodingFunctionByteArray(*arrayType, _fromMemory);
|
||||||
|
else
|
||||||
|
return abiDecodingFunctionArray(*arrayType, _fromMemory);
|
||||||
|
}
|
||||||
|
else if (auto const* structType = dynamic_cast<StructType const*>(decodingType.get()))
|
||||||
|
return abiDecodingFunctionStruct(*structType, _fromMemory);
|
||||||
|
else if (auto const* functionType = dynamic_cast<FunctionType const*>(decodingType.get()))
|
||||||
|
return abiDecodingFunctionFunctionType(*functionType, _fromMemory, _forUseOnStack);
|
||||||
|
else
|
||||||
|
return abiDecodingFunctionValueType(_type, _fromMemory);
|
||||||
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::abiDecodingFunctionValueType(const Type& _type, bool _fromMemory)
|
||||||
|
{
|
||||||
|
TypePointer decodingType = _type.decodingType();
|
||||||
|
solAssert(decodingType, "");
|
||||||
|
solAssert(decodingType->sizeOnStack() == 1, "");
|
||||||
|
solAssert(decodingType->isValueType(), "");
|
||||||
|
solAssert(decodingType->calldataEncodedSize() == 32, "");
|
||||||
|
solAssert(!decodingType->isDynamicallyEncoded(), "");
|
||||||
|
|
||||||
|
string functionName =
|
||||||
|
"abi_decode_" +
|
||||||
|
_type.identifier() +
|
||||||
|
(_fromMemory ? "_fromMemory" : "");
|
||||||
|
return createFunction(functionName, [&]() {
|
||||||
|
Whiskers templ(R"(
|
||||||
|
function <functionName>(offset, end) -> value {
|
||||||
|
value := <cleanup>(<load>(offset))
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
templ("functionName", functionName);
|
||||||
|
templ("load", _fromMemory ? "mload" : "calldataload");
|
||||||
|
// Cleanup itself should use the type and not decodingType, because e.g.
|
||||||
|
// the decoding type of an enum is a plain int.
|
||||||
|
templ("cleanup", cleanupFunction(_type, true));
|
||||||
|
return templ.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::abiDecodingFunctionArray(ArrayType const& _type, bool _fromMemory)
|
||||||
|
{
|
||||||
|
solAssert(_type.dataStoredIn(DataLocation::Memory), "");
|
||||||
|
solAssert(!_type.isByteArray(), "");
|
||||||
|
|
||||||
|
string functionName =
|
||||||
|
"abi_decode_" +
|
||||||
|
_type.identifier() +
|
||||||
|
(_fromMemory ? "_fromMemory" : "");
|
||||||
|
|
||||||
|
solAssert(!_type.dataStoredIn(DataLocation::Storage), "");
|
||||||
|
|
||||||
|
return createFunction(functionName, [&]() {
|
||||||
|
string load = _fromMemory ? "mload" : "calldataload";
|
||||||
|
bool dynamicBase = _type.baseType()->isDynamicallyEncoded();
|
||||||
|
Whiskers templ(
|
||||||
|
R"(
|
||||||
|
// <readableTypeName>
|
||||||
|
function <functionName>(offset, end) -> array {
|
||||||
|
switch slt(add(offset, 0x1f), end) case 0 { revert(0, 0) }
|
||||||
|
let length := <retrieveLength>
|
||||||
|
array := <allocate>(<allocationSize>(length))
|
||||||
|
let dst := array
|
||||||
|
<storeLength> // might update offset and dst
|
||||||
|
let src := offset
|
||||||
|
<staticBoundsCheck>
|
||||||
|
for { let i := 0 } lt(i, length) { i := add(i, 1) }
|
||||||
|
{
|
||||||
|
let elementPos := <retrieveElementPos>
|
||||||
|
mstore(dst, <decodingFun>(elementPos, end))
|
||||||
|
dst := add(dst, 0x20)
|
||||||
|
src := add(src, <baseEncodedSize>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
templ("functionName", functionName);
|
||||||
|
templ("readableTypeName", _type.toString(true));
|
||||||
|
templ("retrieveLength", !_type.isDynamicallySized() ? toCompactHexWithPrefix(_type.length()) : load + "(offset)");
|
||||||
|
templ("allocate", allocationFunction());
|
||||||
|
templ("allocationSize", arrayAllocationSizeFunction(_type));
|
||||||
|
if (_type.isDynamicallySized())
|
||||||
|
templ("storeLength", "mstore(array, length) offset := add(offset, 0x20) dst := add(dst, 0x20)");
|
||||||
|
else
|
||||||
|
templ("storeLength", "");
|
||||||
|
if (dynamicBase)
|
||||||
|
{
|
||||||
|
templ("staticBoundsCheck", "");
|
||||||
|
templ("retrieveElementPos", "add(offset, " + load + "(src))");
|
||||||
|
templ("baseEncodedSize", "0x20");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string baseEncodedSize = toCompactHexWithPrefix(_type.baseType()->calldataEncodedSize());
|
||||||
|
templ("staticBoundsCheck", "switch gt(add(src, mul(length, " + baseEncodedSize + ")), end) case 1 { revert(0, 0) }");
|
||||||
|
templ("retrieveElementPos", "src");
|
||||||
|
templ("baseEncodedSize", baseEncodedSize);
|
||||||
|
}
|
||||||
|
templ("decodingFun", abiDecodingFunction(*_type.baseType(), _fromMemory, false));
|
||||||
|
return templ.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::abiDecodingFunctionCalldataArray(ArrayType const& _type)
|
||||||
|
{
|
||||||
|
// This does not work with arrays of complex types - the array access
|
||||||
|
// is not yet implemented in Solidity.
|
||||||
|
solAssert(_type.dataStoredIn(DataLocation::CallData), "");
|
||||||
|
if (!_type.isDynamicallySized())
|
||||||
|
solAssert(_type.length() < u256("0xffffffffffffffff"), "");
|
||||||
|
solAssert(!_type.baseType()->isDynamicallyEncoded(), "");
|
||||||
|
solAssert(_type.baseType()->calldataEncodedSize() < u256("0xffffffffffffffff"), "");
|
||||||
|
|
||||||
|
string functionName =
|
||||||
|
"abi_decode_" +
|
||||||
|
_type.identifier();
|
||||||
|
return createFunction(functionName, [&]() {
|
||||||
|
string templ;
|
||||||
|
if (_type.isDynamicallySized())
|
||||||
|
templ = R"(
|
||||||
|
// <readableTypeName>
|
||||||
|
function <functionName>(offset, end) -> arrayPos, length {
|
||||||
|
switch slt(add(offset, 0x1f), end) case 0 { revert(0, 0) }
|
||||||
|
length := calldataload(offset)
|
||||||
|
switch gt(length, 0xffffffffffffffff) case 1 { revert(0, 0) }
|
||||||
|
arrayPos := add(offset, 0x20)
|
||||||
|
switch gt(add(arrayPos, mul(<length>, <baseEncodedSize>)), end) case 1 { revert(0, 0) }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
else
|
||||||
|
templ = R"(
|
||||||
|
// <readableTypeName>
|
||||||
|
function <functionName>(offset, end) -> arrayPos {
|
||||||
|
arrayPos := offset
|
||||||
|
switch gt(add(arrayPos, mul(<length>, <baseEncodedSize>)), end) case 1 { revert(0, 0) }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
Whiskers w{templ};
|
||||||
|
w("functionName", functionName);
|
||||||
|
w("readableTypeName", _type.toString(true));
|
||||||
|
w("baseEncodedSize", toCompactHexWithPrefix(_type.isByteArray() ? 1 : _type.baseType()->calldataEncodedSize()));
|
||||||
|
w("length", _type.isDynamicallyEncoded() ? "length" : toCompactHexWithPrefix(_type.length()));
|
||||||
|
return w.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::abiDecodingFunctionByteArray(ArrayType const& _type, bool _fromMemory)
|
||||||
|
{
|
||||||
|
solAssert(_type.dataStoredIn(DataLocation::Memory), "");
|
||||||
|
solAssert(_type.isByteArray(), "");
|
||||||
|
|
||||||
|
string functionName =
|
||||||
|
"abi_decode_" +
|
||||||
|
_type.identifier() +
|
||||||
|
(_fromMemory ? "_fromMemory" : "");
|
||||||
|
|
||||||
|
return createFunction(functionName, [&]() {
|
||||||
|
Whiskers templ(
|
||||||
|
R"(
|
||||||
|
function <functionName>(offset, end) -> array {
|
||||||
|
switch slt(add(offset, 0x1f), end) case 0 { revert(0, 0) }
|
||||||
|
let length := <load>(offset)
|
||||||
|
array := <allocate>(<allocationSize>(length))
|
||||||
|
mstore(array, length)
|
||||||
|
let src := add(offset, 0x20)
|
||||||
|
let dst := add(array, 0x20)
|
||||||
|
switch gt(add(src, length), end) case 1 { revert(0, 0) }
|
||||||
|
<copyToMemFun>(src, dst, length)
|
||||||
|
}
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
templ("functionName", functionName);
|
||||||
|
templ("load", _fromMemory ? "mload" : "calldataload");
|
||||||
|
templ("allocate", allocationFunction());
|
||||||
|
templ("allocationSize", arrayAllocationSizeFunction(_type));
|
||||||
|
templ("copyToMemFun", copyToMemoryFunction(!_fromMemory));
|
||||||
|
return templ.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::abiDecodingFunctionStruct(StructType const& _type, bool _fromMemory)
|
||||||
|
{
|
||||||
|
string functionName =
|
||||||
|
"abi_decode_" +
|
||||||
|
_type.identifier() +
|
||||||
|
(_fromMemory ? "_fromMemory" : "");
|
||||||
|
|
||||||
|
solUnimplementedAssert(!_type.dataStoredIn(DataLocation::CallData), "");
|
||||||
|
|
||||||
|
return createFunction(functionName, [&]() {
|
||||||
|
Whiskers templ(R"(
|
||||||
|
// <readableTypeName>
|
||||||
|
function <functionName>(headStart, end) -> value {
|
||||||
|
switch slt(sub(end, headStart), <minimumSize>) case 1 { revert(0, 0) }
|
||||||
|
value := <allocate>(<memorySize>)
|
||||||
|
<#members>
|
||||||
|
{
|
||||||
|
// <memberName>
|
||||||
|
<decode>
|
||||||
|
}
|
||||||
|
</members>
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
templ("functionName", functionName);
|
||||||
|
templ("readableTypeName", _type.toString(true));
|
||||||
|
templ("allocate", allocationFunction());
|
||||||
|
solAssert(_type.memorySize() < u256("0xffffffffffffffff"), "");
|
||||||
|
templ("memorySize", toCompactHexWithPrefix(_type.memorySize()));
|
||||||
|
size_t headPos = 0;
|
||||||
|
vector<map<string, string>> members;
|
||||||
|
for (auto const& member: _type.members(nullptr))
|
||||||
|
{
|
||||||
|
solAssert(member.type, "");
|
||||||
|
solAssert(member.type->canLiveOutsideStorage(), "");
|
||||||
|
auto decodingType = member.type->decodingType();
|
||||||
|
solAssert(decodingType, "");
|
||||||
|
bool dynamic = decodingType->isDynamicallyEncoded();
|
||||||
|
Whiskers memberTempl(
|
||||||
|
dynamic ?
|
||||||
|
R"(
|
||||||
|
let offset := <load>(add(headStart, <pos>))
|
||||||
|
switch gt(offset, 0xffffffffffffffff) case 1 { revert(0, 0) }
|
||||||
|
mstore(add(value, <memoryOffset>), <abiDecode>(add(headStart, offset), end))
|
||||||
|
)" :
|
||||||
|
R"(
|
||||||
|
let offset := <pos>
|
||||||
|
mstore(add(value, <memoryOffset>), <abiDecode>(add(headStart, offset), end))
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
memberTempl("load", _fromMemory ? "mload" : "calldataload");
|
||||||
|
memberTempl("pos", to_string(headPos));
|
||||||
|
memberTempl("memoryOffset", toCompactHexWithPrefix(_type.memoryOffsetOfMember(member.name)));
|
||||||
|
memberTempl("abiDecode", abiDecodingFunction(*member.type, _fromMemory, false));
|
||||||
|
|
||||||
|
members.push_back({});
|
||||||
|
members.back()["decode"] = memberTempl.render();
|
||||||
|
members.back()["memberName"] = member.name;
|
||||||
|
headPos += dynamic ? 0x20 : decodingType->calldataEncodedSize();
|
||||||
|
}
|
||||||
|
templ("members", members);
|
||||||
|
templ("minimumSize", toCompactHexWithPrefix(headPos));
|
||||||
|
return templ.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::abiDecodingFunctionFunctionType(FunctionType const& _type, bool _fromMemory, bool _forUseOnStack)
|
||||||
|
{
|
||||||
|
solAssert(_type.kind() == FunctionType::Kind::External, "");
|
||||||
|
|
||||||
|
string functionName =
|
||||||
|
"abi_decode_" +
|
||||||
|
_type.identifier() +
|
||||||
|
(_fromMemory ? "_fromMemory" : "") +
|
||||||
|
(_forUseOnStack ? "_onStack" : "");
|
||||||
|
|
||||||
|
return createFunction(functionName, [&]() {
|
||||||
|
if (_forUseOnStack)
|
||||||
|
{
|
||||||
|
return Whiskers(R"(
|
||||||
|
function <functionName>(offset, end) -> addr, function_selector {
|
||||||
|
addr, function_selector := <splitExtFun>(<load>(offset))
|
||||||
|
}
|
||||||
|
)")
|
||||||
|
("functionName", functionName)
|
||||||
|
("load", _fromMemory ? "mload" : "calldataload")
|
||||||
|
("splitExtFun", splitExternalFunctionIdFunction())
|
||||||
|
.render();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Whiskers(R"(
|
||||||
|
function <functionName>(offset, end) -> fun {
|
||||||
|
fun := <cleanExtFun>(<load>(offset))
|
||||||
|
}
|
||||||
|
)")
|
||||||
|
("functionName", functionName)
|
||||||
|
("load", _fromMemory ? "mload" : "calldataload")
|
||||||
|
("cleanExtFun", cleanupCombinedExternalFunctionIdFunction())
|
||||||
|
.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
string ABIFunctions::copyToMemoryFunction(bool _fromCalldata)
|
string ABIFunctions::copyToMemoryFunction(bool _fromCalldata)
|
||||||
{
|
{
|
||||||
string functionName = "copy_" + string(_fromCalldata ? "calldata" : "memory") + "_to_memory";
|
string functionName = "copy_" + string(_fromCalldata ? "calldata" : "memory") + "_to_memory";
|
||||||
@ -1098,6 +1493,33 @@ string ABIFunctions::arrayLengthFunction(ArrayType const& _type)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::arrayAllocationSizeFunction(ArrayType const& _type)
|
||||||
|
{
|
||||||
|
solAssert(_type.dataStoredIn(DataLocation::Memory), "");
|
||||||
|
string functionName = "array_allocation_size_" + _type.identifier();
|
||||||
|
return createFunction(functionName, [&]() {
|
||||||
|
Whiskers w(R"(
|
||||||
|
function <functionName>(length) -> size {
|
||||||
|
// Make sure we can allocate memory without overflow
|
||||||
|
switch gt(length, 0xffffffffffffffff) case 1 { revert(0, 0) }
|
||||||
|
size := <allocationSize>
|
||||||
|
<addLengthSlot>
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
w("functionName", functionName);
|
||||||
|
if (_type.isByteArray())
|
||||||
|
// Round up
|
||||||
|
w("allocationSize", "and(add(length, 0x1f), not(0x1f))");
|
||||||
|
else
|
||||||
|
w("allocationSize", "mul(length, 0x20)");
|
||||||
|
if (_type.isDynamicallySized())
|
||||||
|
w("addLengthSlot", "size := add(size, 0x20)");
|
||||||
|
else
|
||||||
|
w("addLengthSlot", "");
|
||||||
|
return w.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
string ABIFunctions::arrayDataAreaFunction(ArrayType const& _type)
|
string ABIFunctions::arrayDataAreaFunction(ArrayType const& _type)
|
||||||
{
|
{
|
||||||
string functionName = "array_dataslot_" + _type.identifier();
|
string functionName = "array_dataslot_" + _type.identifier();
|
||||||
@ -1189,6 +1611,25 @@ string ABIFunctions::nextArrayElementFunction(ArrayType const& _type)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string ABIFunctions::allocationFunction()
|
||||||
|
{
|
||||||
|
string functionName = "allocateMemory";
|
||||||
|
return createFunction(functionName, [&]() {
|
||||||
|
return Whiskers(R"(
|
||||||
|
function <functionName>(size) -> memPtr {
|
||||||
|
memPtr := mload(<freeMemoryPointer>)
|
||||||
|
let newFreePtr := add(memPtr, size)
|
||||||
|
// protect against overflow
|
||||||
|
switch or(gt(newFreePtr, 0xffffffffffffffff), lt(newFreePtr, memPtr)) case 1 { revert(0, 0) }
|
||||||
|
mstore(<freeMemoryPointer>, newFreePtr)
|
||||||
|
}
|
||||||
|
)")
|
||||||
|
("freeMemoryPointer", to_string(CompilerUtils::freeMemoryPointer))
|
||||||
|
("functionName", functionName)
|
||||||
|
.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
string ABIFunctions::createFunction(string const& _name, function<string ()> const& _creator)
|
string ABIFunctions::createFunction(string const& _name, function<string ()> const& _creator)
|
||||||
{
|
{
|
||||||
if (!m_requestedFunctions.count(_name))
|
if (!m_requestedFunctions.count(_name))
|
||||||
|
@ -66,6 +66,16 @@ public:
|
|||||||
bool _encodeAsLibraryTypes = false
|
bool _encodeAsLibraryTypes = false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// @returns name of an assembly function to ABI-decode values of @a _types
|
||||||
|
/// into memory. If @a _fromMemory is true, decodes from memory instead of
|
||||||
|
/// from calldata.
|
||||||
|
/// Can allocate memory.
|
||||||
|
/// Inputs: <source_offset> <source_end> (layout reversed on stack)
|
||||||
|
/// Outputs: <value0> <value1> ... <valuen>
|
||||||
|
/// The values represent stack slots. If a type occupies more or less than one
|
||||||
|
/// stack slot, it takes exactly that number of values.
|
||||||
|
std::string tupleDecoder(TypePointers const& _types, bool _fromMemory = false);
|
||||||
|
|
||||||
/// @returns concatenation of all generated functions.
|
/// @returns concatenation of all generated functions.
|
||||||
std::string requestedFunctions();
|
std::string requestedFunctions();
|
||||||
|
|
||||||
@ -87,6 +97,10 @@ private:
|
|||||||
/// for use in the ABI.
|
/// for use in the ABI.
|
||||||
std::string combineExternalFunctionIdFunction();
|
std::string combineExternalFunctionIdFunction();
|
||||||
|
|
||||||
|
/// @returns a function that splits the address and selector from a single value
|
||||||
|
/// for use in the ABI.
|
||||||
|
std::string splitExternalFunctionIdFunction();
|
||||||
|
|
||||||
/// @returns the name of the ABI encoding function with the given type
|
/// @returns the name of the ABI encoding function with the given type
|
||||||
/// and queues the generation of the function to the requested functions.
|
/// and queues the generation of the function to the requested functions.
|
||||||
/// @param _fromStack if false, the input value was just loaded from storage
|
/// @param _fromStack if false, the input value was just loaded from storage
|
||||||
@ -146,6 +160,31 @@ private:
|
|||||||
bool _fromStack
|
bool _fromStack
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// @returns the name of the ABI decoding function for the given type
|
||||||
|
/// and queues the generation of the function to the requested functions.
|
||||||
|
/// The caller has to ensure that no out of bounds access (at least to the static
|
||||||
|
/// part) can happen inside this function.
|
||||||
|
/// @param _fromMemory if decoding from memory instead of from calldata
|
||||||
|
/// @param _forUseOnStack if the decoded value is stored on stack or in memory.
|
||||||
|
std::string abiDecodingFunction(
|
||||||
|
Type const& _Type,
|
||||||
|
bool _fromMemory,
|
||||||
|
bool _forUseOnStack
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Part of @a abiDecodingFunction for value types.
|
||||||
|
std::string abiDecodingFunctionValueType(Type const& _type, bool _fromMemory);
|
||||||
|
/// Part of @a abiDecodingFunction for "regular" array types.
|
||||||
|
std::string abiDecodingFunctionArray(ArrayType const& _type, bool _fromMemory);
|
||||||
|
/// Part of @a abiDecodingFunction for calldata array types.
|
||||||
|
std::string abiDecodingFunctionCalldataArray(ArrayType const& _type);
|
||||||
|
/// Part of @a abiDecodingFunction for byte array types.
|
||||||
|
std::string abiDecodingFunctionByteArray(ArrayType const& _type, bool _fromMemory);
|
||||||
|
/// Part of @a abiDecodingFunction for struct types.
|
||||||
|
std::string abiDecodingFunctionStruct(StructType const& _type, bool _fromMemory);
|
||||||
|
/// Part of @a abiDecodingFunction for array types.
|
||||||
|
std::string abiDecodingFunctionFunctionType(FunctionType const& _type, bool _fromMemory, bool _forUseOnStack);
|
||||||
|
|
||||||
/// @returns a function that copies raw bytes of dynamic length from calldata
|
/// @returns a function that copies raw bytes of dynamic length from calldata
|
||||||
/// or memory to memory.
|
/// or memory to memory.
|
||||||
/// Pads with zeros and might write more than exactly length.
|
/// Pads with zeros and might write more than exactly length.
|
||||||
@ -158,6 +197,10 @@ private:
|
|||||||
std::string roundUpFunction();
|
std::string roundUpFunction();
|
||||||
|
|
||||||
std::string arrayLengthFunction(ArrayType const& _type);
|
std::string arrayLengthFunction(ArrayType const& _type);
|
||||||
|
/// @returns the name of a function that computes the number of bytes required
|
||||||
|
/// to store an array in memory given its length (internally encoded, not ABI encoded).
|
||||||
|
/// The function reverts for too large lengthes.
|
||||||
|
std::string arrayAllocationSizeFunction(ArrayType const& _type);
|
||||||
/// @returns the name of a function that converts a storage slot number
|
/// @returns the name of a function that converts a storage slot number
|
||||||
/// or a memory pointer to the slot number / memory pointer for the data position of an array
|
/// or a memory pointer to the slot number / memory pointer for the data position of an array
|
||||||
/// which is stored in that slot / memory area.
|
/// which is stored in that slot / memory area.
|
||||||
@ -166,6 +209,12 @@ private:
|
|||||||
/// Only works for memory arrays and storage arrays that store one item per slot.
|
/// Only works for memory arrays and storage arrays that store one item per slot.
|
||||||
std::string nextArrayElementFunction(ArrayType const& _type);
|
std::string nextArrayElementFunction(ArrayType const& _type);
|
||||||
|
|
||||||
|
/// @returns the name of a function that allocates memory.
|
||||||
|
/// Modifies the "free memory pointer"
|
||||||
|
/// Arguments: size
|
||||||
|
/// Return value: pointer
|
||||||
|
std::string allocationFunction();
|
||||||
|
|
||||||
/// Helper function that uses @a _creator to create a function and add it to
|
/// Helper function that uses @a _creator to create a function and add it to
|
||||||
/// @a m_requestedFunctions if it has not been created yet and returns @a _name in both
|
/// @a m_requestedFunctions if it has not been created yet and returns @a _name in both
|
||||||
/// cases.
|
/// cases.
|
||||||
|
@ -319,6 +319,23 @@ void CompilerUtils::abiEncodeV2(
|
|||||||
m_context << ret.tag();
|
m_context << ret.tag();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CompilerUtils::abiDecodeV2(TypePointers const& _parameterTypes, bool _fromMemory)
|
||||||
|
{
|
||||||
|
// stack: <source_offset>
|
||||||
|
auto ret = m_context.pushNewTag();
|
||||||
|
m_context << Instruction::SWAP1;
|
||||||
|
if (_fromMemory)
|
||||||
|
// TODO pass correct size for the memory case
|
||||||
|
m_context << (u256(1) << 63);
|
||||||
|
else
|
||||||
|
m_context << Instruction::CALLDATASIZE;
|
||||||
|
m_context << Instruction::SWAP1;
|
||||||
|
string decoderName = m_context.abiFunctions().tupleDecoder(_parameterTypes, _fromMemory);
|
||||||
|
m_context.appendJumpTo(m_context.namedTag(decoderName));
|
||||||
|
m_context.adjustStackOffset(int(sizeOnStack(_parameterTypes)) - 3);
|
||||||
|
m_context << ret.tag();
|
||||||
|
}
|
||||||
|
|
||||||
void CompilerUtils::zeroInitialiseMemoryArray(ArrayType const& _type)
|
void CompilerUtils::zeroInitialiseMemoryArray(ArrayType const& _type)
|
||||||
{
|
{
|
||||||
auto repeat = m_context.newTag();
|
auto repeat = m_context.newTag();
|
||||||
|
@ -146,6 +146,13 @@ public:
|
|||||||
bool _encodeAsLibraryTypes = false
|
bool _encodeAsLibraryTypes = false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Decodes data from ABI encoding into internal encoding. If @a _fromMemory is set to true,
|
||||||
|
/// the data is taken from memory instead of from calldata.
|
||||||
|
/// Can allocate memory.
|
||||||
|
/// Stack pre: <source_offset>
|
||||||
|
/// Stack post: <value0> <value1> ... <valuen>
|
||||||
|
void abiDecodeV2(TypePointers const& _parameterTypes, bool _fromMemory = false);
|
||||||
|
|
||||||
/// Zero-initialises (the data part of) an already allocated memory array.
|
/// Zero-initialises (the data part of) an already allocated memory array.
|
||||||
/// Length has to be nonzero!
|
/// Length has to be nonzero!
|
||||||
/// Stack pre: <length> <memptr>
|
/// Stack pre: <length> <memptr>
|
||||||
|
@ -322,6 +322,15 @@ void ContractCompiler::appendCalldataUnpacker(TypePointers const& _typeParameter
|
|||||||
{
|
{
|
||||||
// We do not check the calldata size, everything is zero-padded
|
// We do not check the calldata size, everything is zero-padded
|
||||||
|
|
||||||
|
if (m_context.experimentalFeatureActive(ExperimentalFeature::ABIEncoderV2))
|
||||||
|
{
|
||||||
|
// Use the new JULIA-based decoding function
|
||||||
|
auto stackHeightBefore = m_context.stackHeight();
|
||||||
|
CompilerUtils(m_context).abiDecodeV2(_typeParameters, _fromMemory);
|
||||||
|
solAssert(m_context.stackHeight() - stackHeightBefore == CompilerUtils(m_context).sizeOnStack(_typeParameters) - 1, "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//@todo this does not yet support nested dynamic arrays
|
//@todo this does not yet support nested dynamic arrays
|
||||||
|
|
||||||
// Retain the offset pointer as base_offset, the point from which the data offsets are computed.
|
// Retain the offset pointer as base_offset, the point from which the data offsets are computed.
|
||||||
|
@ -84,12 +84,22 @@ public:
|
|||||||
return callFallbackWithValue(0);
|
return callFallbackWithValue(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bytes const& callContractFunctionWithValueNoEncoding(std::string _sig, u256 const& _value, bytes const& _arguments)
|
||||||
|
{
|
||||||
|
FixedHash<4> hash(dev::keccak256(_sig));
|
||||||
|
sendMessage(hash.asBytes() + _arguments, false, _value);
|
||||||
|
return m_output;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes const& callContractFunctionNoEncoding(std::string _sig, bytes const& _arguments)
|
||||||
|
{
|
||||||
|
return callContractFunctionWithValueNoEncoding(_sig, 0, _arguments);
|
||||||
|
}
|
||||||
|
|
||||||
template <class... Args>
|
template <class... Args>
|
||||||
bytes const& callContractFunctionWithValue(std::string _sig, u256 const& _value, Args const&... _arguments)
|
bytes const& callContractFunctionWithValue(std::string _sig, u256 const& _value, Args const&... _arguments)
|
||||||
{
|
{
|
||||||
FixedHash<4> hash(dev::keccak256(_sig));
|
return callContractFunctionWithValueNoEncoding(_sig, _value, encodeArgs(_arguments...));
|
||||||
sendMessage(hash.asBytes() + encodeArgs(_arguments...), false, _value);
|
|
||||||
return m_output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template <class... Args>
|
template <class... Args>
|
||||||
|
@ -57,6 +57,7 @@ test_suite* init_unit_test_suite( int /*argc*/, char* /*argv*/[] )
|
|||||||
if (dev::test::Options::get().disableIPC)
|
if (dev::test::Options::get().disableIPC)
|
||||||
{
|
{
|
||||||
for (auto suite: {
|
for (auto suite: {
|
||||||
|
"ABIDecoderTest",
|
||||||
"ABIEncoderTest",
|
"ABIEncoderTest",
|
||||||
"SolidityAuctionRegistrar",
|
"SolidityAuctionRegistrar",
|
||||||
"SolidityFixedFeeRegistrar",
|
"SolidityFixedFeeRegistrar",
|
||||||
|
794
test/libsolidity/ABIDecoderTests.cpp
Normal file
794
test/libsolidity/ABIDecoderTests.cpp
Normal file
@ -0,0 +1,794 @@
|
|||||||
|
/*
|
||||||
|
This file is part of solidity.
|
||||||
|
|
||||||
|
solidity is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
solidity is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with solidity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Unit tests for Solidity's ABI decoder.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
#include <boost/test/unit_test.hpp>
|
||||||
|
#include <libsolidity/interface/Exceptions.h>
|
||||||
|
#include <test/libsolidity/SolidityExecutionFramework.h>
|
||||||
|
|
||||||
|
#include <test/libsolidity/ABITestsCommon.h>
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
using namespace std::placeholders;
|
||||||
|
using namespace dev::test;
|
||||||
|
|
||||||
|
namespace dev
|
||||||
|
{
|
||||||
|
namespace solidity
|
||||||
|
{
|
||||||
|
namespace test
|
||||||
|
{
|
||||||
|
|
||||||
|
BOOST_FIXTURE_TEST_SUITE(ABIDecoderTest, SolidityExecutionFramework)
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(both_encoders_macro)
|
||||||
|
{
|
||||||
|
// This tests that the "both decoders macro" at least runs twice and
|
||||||
|
// modifies the source.
|
||||||
|
string sourceCode;
|
||||||
|
int runs = 0;
|
||||||
|
BOTH_ENCODERS(runs++;)
|
||||||
|
BOOST_CHECK(sourceCode == NewEncoderPragma);
|
||||||
|
BOOST_CHECK_EQUAL(runs, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(value_types)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function f(uint a, uint16 b, uint24 c, int24 d, bytes3 x, bool e, C g) public returns (uint) {
|
||||||
|
if (a != 1) return 1;
|
||||||
|
if (b != 2) return 2;
|
||||||
|
if (c != 3) return 3;
|
||||||
|
if (d != 4) return 4;
|
||||||
|
if (x != "abc") return 5;
|
||||||
|
if (e != true) return 6;
|
||||||
|
if (g != this) return 7;
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
ABI_CHECK(callContractFunction(
|
||||||
|
"f(uint256,uint16,uint24,int24,bytes3,bool,address)",
|
||||||
|
1, 2, 3, 4, string("abc"), true, u160(m_contractAddress)
|
||||||
|
), encodeArgs(u256(20)));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(enums)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
enum E { A, B }
|
||||||
|
function f(E e) public pure returns (uint x) {
|
||||||
|
assembly { x := e }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
bool newDecoder = false;
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
ABI_CHECK(callContractFunction("f(uint8)", 0), encodeArgs(u256(0)));
|
||||||
|
ABI_CHECK(callContractFunction("f(uint8)", 1), encodeArgs(u256(1)));
|
||||||
|
// The old decoder was not as strict about enums
|
||||||
|
ABI_CHECK(callContractFunction("f(uint8)", 2), (newDecoder ? encodeArgs() : encodeArgs(2)));
|
||||||
|
ABI_CHECK(callContractFunction("f(uint8)", u256(-1)), (newDecoder? encodeArgs() : encodeArgs(u256(0xff))));
|
||||||
|
newDecoder = true;
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(cleanup)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function f(uint16 a, int16 b, address c, bytes3 d, bool e)
|
||||||
|
public pure returns (uint v, uint w, uint x, uint y, uint z) {
|
||||||
|
assembly { v := a w := b x := c y := d z := e}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunction("f(uint16,int16,address,bytes3,bool)", 1, 2, 3, "a", true),
|
||||||
|
encodeArgs(u256(1), u256(2), u256(3), string("a"), true)
|
||||||
|
);
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunction(
|
||||||
|
"f(uint16,int16,address,bytes3,bool)",
|
||||||
|
u256(0xffffff), u256(0x1ffff), u256(-1), string("abcd"), u256(4)
|
||||||
|
),
|
||||||
|
encodeArgs(u256(0xffff), u256(-1), (u256(1) << 160) - 1, string("abc"), true)
|
||||||
|
);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(fixed_arrays)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function f(uint16[3] a, uint16[2][3] b, uint i, uint j, uint k)
|
||||||
|
public pure returns (uint, uint) {
|
||||||
|
return (a[i], b[j][k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
bytes args = encodeArgs(
|
||||||
|
1, 2, 3,
|
||||||
|
11, 12,
|
||||||
|
21, 22,
|
||||||
|
31, 32,
|
||||||
|
1, 2, 1
|
||||||
|
);
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunction("f(uint16[3],uint16[2][3],uint256,uint256,uint256)", args),
|
||||||
|
encodeArgs(u256(2), u256(32))
|
||||||
|
);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(dynamic_arrays)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function f(uint a, uint16[] b, uint c)
|
||||||
|
public pure returns (uint, uint, uint) {
|
||||||
|
return (b.length, b[a], c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
bytes args = encodeArgs(
|
||||||
|
6, 0x60, 9,
|
||||||
|
7,
|
||||||
|
11, 12, 13, 14, 15, 16, 17
|
||||||
|
);
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunction("f(uint256,uint16[],uint256)", args),
|
||||||
|
encodeArgs(u256(7), u256(17), u256(9))
|
||||||
|
);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(dynamic_nested_arrays)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function f(uint a, uint16[][] b, uint[2][][3] c, uint d)
|
||||||
|
public pure returns (uint, uint, uint, uint, uint, uint, uint) {
|
||||||
|
return (a, b.length, b[1].length, b[1][1], c[1].length, c[1][1][1], d);
|
||||||
|
}
|
||||||
|
function test() view returns (uint, uint, uint, uint, uint, uint, uint) {
|
||||||
|
uint16[][] memory b = new uint16[][](3);
|
||||||
|
b[0] = new uint16[](2);
|
||||||
|
b[0][0] = 0x55;
|
||||||
|
b[0][1] = 0x56;
|
||||||
|
b[1] = new uint16[](4);
|
||||||
|
b[1][0] = 0x65;
|
||||||
|
b[1][1] = 0x66;
|
||||||
|
b[1][2] = 0x67;
|
||||||
|
b[1][3] = 0x68;
|
||||||
|
|
||||||
|
uint[2][][3] memory c;
|
||||||
|
c[0] = new uint[2][](1);
|
||||||
|
c[0][0][1] = 0x75;
|
||||||
|
c[1] = new uint[2][](5);
|
||||||
|
c[1][1][1] = 0x85;
|
||||||
|
|
||||||
|
return this.f(0x12, b, c, 0x13);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
bytes args = encodeArgs(
|
||||||
|
0x12, 4 * 0x20, 17 * 0x20, 0x13,
|
||||||
|
// b
|
||||||
|
3, 3 * 0x20, 6 * 0x20, 11 * 0x20,
|
||||||
|
2, 85, 86,
|
||||||
|
4, 101, 102, 103, 104,
|
||||||
|
0,
|
||||||
|
// c
|
||||||
|
3 * 0x20, 6 * 0x20, 17 * 0x20,
|
||||||
|
1, 0, 117,
|
||||||
|
5, 0, 0, 0, 133, 0, 0, 0, 0, 0, 0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes expectation = encodeArgs(0x12, 3, 4, 0x66, 5, 0x85, 0x13);
|
||||||
|
ABI_CHECK(callContractFunction("test()"), expectation);
|
||||||
|
ABI_CHECK(callContractFunction("f(uint256,uint16[][],uint256[2][][3],uint256)", args), expectation);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(byte_arrays)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function f(uint a, bytes b, uint c)
|
||||||
|
public pure returns (uint, uint, byte, uint) {
|
||||||
|
return (a, b.length, b[3], c);
|
||||||
|
}
|
||||||
|
|
||||||
|
function f_external(uint a, bytes b, uint c)
|
||||||
|
external pure returns (uint, uint, byte, uint) {
|
||||||
|
return (a, b.length, b[3], c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
bytes args = encodeArgs(
|
||||||
|
6, 0x60, 9,
|
||||||
|
7, "abcdefg"
|
||||||
|
);
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunction("f(uint256,bytes,uint256)", args),
|
||||||
|
encodeArgs(u256(6), u256(7), "d", 9)
|
||||||
|
);
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunction("f_external(uint256,bytes,uint256)", args),
|
||||||
|
encodeArgs(u256(6), u256(7), "d", 9)
|
||||||
|
);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(calldata_arrays_too_large)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function f(uint a, uint[] b, uint c) external pure returns (uint) {
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
bool newEncoder = false;
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
bytes args = encodeArgs(
|
||||||
|
6, 0x60, 9,
|
||||||
|
(u256(1) << 255) + 2, 1, 2
|
||||||
|
);
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunction("f(uint256,uint256[],uint256)", args),
|
||||||
|
newEncoder ? encodeArgs() : encodeArgs(7)
|
||||||
|
);
|
||||||
|
newEncoder = true;
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(decode_from_memory_simple)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
uint public _a;
|
||||||
|
uint[] public _b;
|
||||||
|
function C(uint a, uint[] b) {
|
||||||
|
_a = a;
|
||||||
|
_b = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode, 0, "C", encodeArgs(
|
||||||
|
7, 0x40,
|
||||||
|
// b
|
||||||
|
3, 0x21, 0x22, 0x23
|
||||||
|
));
|
||||||
|
ABI_CHECK(callContractFunction("_a()"), encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunction("_b(uint256)", 0), encodeArgs(0x21));
|
||||||
|
ABI_CHECK(callContractFunction("_b(uint256)", 1), encodeArgs(0x22));
|
||||||
|
ABI_CHECK(callContractFunction("_b(uint256)", 2), encodeArgs(0x23));
|
||||||
|
ABI_CHECK(callContractFunction("_b(uint256)", 3), encodeArgs());
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(decode_function_type)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract D {
|
||||||
|
function () external returns (uint) public _a;
|
||||||
|
function D(function () external returns (uint) a) {
|
||||||
|
_a = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contract C {
|
||||||
|
function f() returns (uint) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
function g(function () external returns (uint) _f) returns (uint) {
|
||||||
|
return _f();
|
||||||
|
}
|
||||||
|
// uses "decode from memory"
|
||||||
|
function test1() returns (uint) {
|
||||||
|
D d = new D(this.f);
|
||||||
|
return d._a()();
|
||||||
|
}
|
||||||
|
// uses "decode from calldata"
|
||||||
|
function test2() returns (uint) {
|
||||||
|
return this.g(this.f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode, 0, "C");
|
||||||
|
ABI_CHECK(callContractFunction("test1()"), encodeArgs(3));
|
||||||
|
ABI_CHECK(callContractFunction("test2()"), encodeArgs(3));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(decode_function_type_array)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract D {
|
||||||
|
function () external returns (uint)[] public _a;
|
||||||
|
function D(function () external returns (uint)[] a) {
|
||||||
|
_a = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contract E {
|
||||||
|
function () external returns (uint)[3] public _a;
|
||||||
|
function E(function () external returns (uint)[3] a) {
|
||||||
|
_a = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contract C {
|
||||||
|
function f1() public returns (uint) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
function f2() public returns (uint) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
function f3() public returns (uint) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
function g(function () external returns (uint)[] _f, uint i) public returns (uint) {
|
||||||
|
return _f[i]();
|
||||||
|
}
|
||||||
|
function h(function () external returns (uint)[3] _f, uint i) public returns (uint) {
|
||||||
|
return _f[i]();
|
||||||
|
}
|
||||||
|
// uses "decode from memory"
|
||||||
|
function test1_dynamic() public returns (uint) {
|
||||||
|
var x = new function() external returns (uint)[](3);
|
||||||
|
x[0] = this.f1;
|
||||||
|
x[1] = this.f2;
|
||||||
|
x[2] = this.f3;
|
||||||
|
D d = new D(x);
|
||||||
|
return d._a(2)();
|
||||||
|
}
|
||||||
|
function test1_static() public returns (uint) {
|
||||||
|
E e = new E([this.f1, this.f2, this.f3]);
|
||||||
|
return e._a(2)();
|
||||||
|
}
|
||||||
|
// uses "decode from calldata"
|
||||||
|
function test2_dynamic() public returns (uint) {
|
||||||
|
var x = new function() external returns (uint)[](3);
|
||||||
|
x[0] = this.f1;
|
||||||
|
x[1] = this.f2;
|
||||||
|
x[2] = this.f3;
|
||||||
|
return this.g(x, 0);
|
||||||
|
}
|
||||||
|
function test2_static() public returns (uint) {
|
||||||
|
return this.h([this.f1, this.f2, this.f3], 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode, 0, "C");
|
||||||
|
ABI_CHECK(callContractFunction("test1_static()"), encodeArgs(3));
|
||||||
|
ABI_CHECK(callContractFunction("test1_dynamic()"), encodeArgs(3));
|
||||||
|
ABI_CHECK(callContractFunction("test2_static()"), encodeArgs(1));
|
||||||
|
ABI_CHECK(callContractFunction("test2_dynamic()"), encodeArgs(1));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(decode_from_memory_complex)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
uint public _a;
|
||||||
|
uint[] public _b;
|
||||||
|
bytes[2] public _c;
|
||||||
|
function C(uint a, uint[] b, bytes[2] c) {
|
||||||
|
_a = a;
|
||||||
|
_b = b;
|
||||||
|
_c = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode, 0, "C", encodeArgs(
|
||||||
|
7, 0x60, 7 * 0x20,
|
||||||
|
// b
|
||||||
|
3, 0x21, 0x22, 0x23,
|
||||||
|
// c
|
||||||
|
0x40, 0x80,
|
||||||
|
8, string("abcdefgh"),
|
||||||
|
52, string("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
));
|
||||||
|
ABI_CHECK(callContractFunction("_a()"), encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunction("_b(uint256)", 0), encodeArgs(0x21));
|
||||||
|
ABI_CHECK(callContractFunction("_b(uint256)", 1), encodeArgs(0x22));
|
||||||
|
ABI_CHECK(callContractFunction("_b(uint256)", 2), encodeArgs(0x23));
|
||||||
|
ABI_CHECK(callContractFunction("_b(uint256)", 3), encodeArgs());
|
||||||
|
ABI_CHECK(callContractFunction("_c(uint256)", 0), encodeArgs(0x20, 8, string("abcdefgh")));
|
||||||
|
ABI_CHECK(callContractFunction("_c(uint256)", 1), encodeArgs(0x20, 52, string("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ")));
|
||||||
|
ABI_CHECK(callContractFunction("_c(uint256)", 2), encodeArgs());
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(short_input_value_type)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function f(uint a, uint b) public pure returns (uint) { return a; }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
bool newDecoder = false;
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
ABI_CHECK(callContractFunction("f(uint256,uint256)", 1, 2), encodeArgs(1));
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(uint256,uint256)", bytes(64, 0)), encodeArgs(0));
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(uint256,uint256)", bytes(63, 0)), newDecoder ? encodeArgs() : encodeArgs(0));
|
||||||
|
newDecoder = true;
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(short_input_array)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function f(uint[] a) public pure returns (uint) { return 7; }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
bool newDecoder = false;
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(uint256[])", encodeArgs(0x20, 0)), encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(uint256[])", encodeArgs(0x20, 1)), newDecoder ? encodeArgs() : encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(uint256[])", encodeArgs(0x20, 1) + bytes(31, 0)), newDecoder ? encodeArgs() : encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(uint256[])", encodeArgs(0x20, 1) + bytes(32, 0)), encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(uint256[])", encodeArgs(0x20, 2, 5, 6)), encodeArgs(7));
|
||||||
|
newDecoder = true;
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(short_dynamic_input_array)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function f(bytes[1] a) public pure returns (uint) { return 7; }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(bytes[1])", encodeArgs(0x20)), encodeArgs());
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(short_input_bytes)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
function e(bytes a) public pure returns (uint) { return 7; }
|
||||||
|
function f(bytes[] a) public pure returns (uint) { return 7; }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("e(bytes)", encodeArgs(0x20, 7) + bytes(5, 0)), encodeArgs());
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("e(bytes)", encodeArgs(0x20, 7) + bytes(6, 0)), encodeArgs());
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("e(bytes)", encodeArgs(0x20, 7) + bytes(7, 0)), encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("e(bytes)", encodeArgs(0x20, 7) + bytes(8, 0)), encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(bytes[])", encodeArgs(0x20, 1, 0x20, 7) + bytes(5, 0)), encodeArgs());
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(bytes[])", encodeArgs(0x20, 1, 0x20, 7) + bytes(6, 0)), encodeArgs());
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(bytes[])", encodeArgs(0x20, 1, 0x20, 7) + bytes(7, 0)), encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunctionNoEncoding("f(bytes[])", encodeArgs(0x20, 1, 0x20, 7) + bytes(8, 0)), encodeArgs(7));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(cleanup_int_inside_arrays)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
enum E { A, B }
|
||||||
|
function f(uint16[] a) public pure returns (uint r) { assembly { r := mload(add(a, 0x20)) } }
|
||||||
|
function g(int16[] a) public pure returns (uint r) { assembly { r := mload(add(a, 0x20)) } }
|
||||||
|
function h(E[] a) public pure returns (uint r) { assembly { r := mload(add(a, 0x20)) } }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode);
|
||||||
|
ABI_CHECK(callContractFunction("f(uint16[])", 0x20, 1, 7), encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunction("g(int16[])", 0x20, 1, 7), encodeArgs(7));
|
||||||
|
ABI_CHECK(callContractFunction("f(uint16[])", 0x20, 1, u256("0xffff")), encodeArgs(u256("0xffff")));
|
||||||
|
ABI_CHECK(callContractFunction("g(int16[])", 0x20, 1, u256("0xffff")), encodeArgs(u256(-1)));
|
||||||
|
ABI_CHECK(callContractFunction("f(uint16[])", 0x20, 1, u256("0x1ffff")), encodeArgs(u256("0xffff")));
|
||||||
|
ABI_CHECK(callContractFunction("g(int16[])", 0x20, 1, u256("0x10fff")), encodeArgs(u256("0x0fff")));
|
||||||
|
ABI_CHECK(callContractFunction("h(uint8[])", 0x20, 1, 0), encodeArgs(u256(0)));
|
||||||
|
ABI_CHECK(callContractFunction("h(uint8[])", 0x20, 1, 1), encodeArgs(u256(1)));
|
||||||
|
ABI_CHECK(callContractFunction("h(uint8[])", 0x20, 1, 2), encodeArgs());
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(storage_ptr)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
library L {
|
||||||
|
struct S { uint x; uint y; }
|
||||||
|
function f(uint[] storage r, S storage s) public returns (uint, uint, uint, uint) {
|
||||||
|
r[2] = 8;
|
||||||
|
s.x = 7;
|
||||||
|
return (r[0], r[1], s.x, s.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contract C {
|
||||||
|
uint8 x = 3;
|
||||||
|
L.S s;
|
||||||
|
uint[] r;
|
||||||
|
function f() public returns (uint, uint, uint, uint, uint, uint) {
|
||||||
|
r.length = 6;
|
||||||
|
r[0] = 1;
|
||||||
|
r[1] = 2;
|
||||||
|
r[2] = 3;
|
||||||
|
s.x = 11;
|
||||||
|
s.y = 12;
|
||||||
|
var (a, b, c, d) = L.f(r, s);
|
||||||
|
return (r[2], s.x, a, b, c, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
BOTH_ENCODERS(
|
||||||
|
compileAndRun(sourceCode, 0, "L");
|
||||||
|
compileAndRun(sourceCode, 0, "C", bytes(), map<string, Address>{{"L", m_contractAddress}});
|
||||||
|
ABI_CHECK(callContractFunction("f()"), encodeArgs(8, 7, 1, 2, 7, 12));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(struct_simple)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
struct S { uint a; uint8 b; uint8 c; bytes2 d; }
|
||||||
|
function f(S s) public pure returns (uint a, uint b, uint c, uint d) {
|
||||||
|
a = s.a;
|
||||||
|
b = s.b;
|
||||||
|
c = s.c;
|
||||||
|
d = uint(s.d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode, 0, "C");
|
||||||
|
ABI_CHECK(callContractFunction("f((uint256,uint8,uint8,bytes2))", 1, 2, 3, "ab"), encodeArgs(1, 2, 3, 'a' * 0x100 + 'b'));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(struct_cleanup)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
struct S { int16 a; uint8 b; bytes2 c; }
|
||||||
|
function f(S s) public pure returns (uint a, uint b, uint c) {
|
||||||
|
assembly {
|
||||||
|
a := mload(s)
|
||||||
|
b := mload(add(s, 0x20))
|
||||||
|
c := mload(add(s, 0x40))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode, 0, "C");
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunction("f((int16,uint8,bytes2))", 0xff010, 0xff0002, "abcd"),
|
||||||
|
encodeArgs(u256("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff010"), 2, "ab")
|
||||||
|
);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(struct_short)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
struct S { int a; uint b; bytes16 c; }
|
||||||
|
function f(S s) public pure returns (S q) {
|
||||||
|
q = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode, 0, "C");
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunction("f((int256,uint256,bytes16))", 0xff010, 0xff0002, "abcd"),
|
||||||
|
encodeArgs(0xff010, 0xff0002, "abcd")
|
||||||
|
);
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunctionNoEncoding("f((int256,uint256,bytes16))", encodeArgs(0xff010, 0xff0002) + bytes(32, 0)),
|
||||||
|
encodeArgs(0xff010, 0xff0002, 0)
|
||||||
|
);
|
||||||
|
ABI_CHECK(
|
||||||
|
callContractFunctionNoEncoding("f((int256,uint256,bytes16))", encodeArgs(0xff010, 0xff0002) + bytes(31, 0)),
|
||||||
|
encodeArgs()
|
||||||
|
);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(struct_function)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
struct S { function () external returns (uint) f; uint b; }
|
||||||
|
function f(S s) public returns (uint, uint) {
|
||||||
|
return (s.f(), s.b);
|
||||||
|
}
|
||||||
|
function test() public returns (uint, uint) {
|
||||||
|
return this.f(S(this.g, 3));
|
||||||
|
}
|
||||||
|
function g() public returns (uint) { return 7; }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode, 0, "C");
|
||||||
|
ABI_CHECK(callContractFunction("test()"), encodeArgs(7, 3));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(empty_struct)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
struct S { }
|
||||||
|
function f(uint a, S s, uint b) public pure returns (uint x, uint y) {
|
||||||
|
assembly { x := a y := b }
|
||||||
|
}
|
||||||
|
function g() public returns (uint, uint) {
|
||||||
|
return this.f(7, S(), 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode, 0, "C");
|
||||||
|
ABI_CHECK(callContractFunction("f(uint256,(),uint256)", 7, 8), encodeArgs(7, 8));
|
||||||
|
ABI_CHECK(callContractFunction("g()"), encodeArgs(7, 8));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(mediocre_struct)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
struct S { C c; }
|
||||||
|
function f(uint a, S[2] s1, uint b) public returns (uint r1, C r2, uint r3) {
|
||||||
|
r1 = a;
|
||||||
|
r2 = s1[0].c;
|
||||||
|
r3 = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode, 0, "C");
|
||||||
|
string sig = "f(uint256,(address)[2],uint256)";
|
||||||
|
ABI_CHECK(callContractFunction(sig,
|
||||||
|
7, u256(u160(m_contractAddress)), 0, 8
|
||||||
|
), encodeArgs(7, u256(u160(m_contractAddress)), 8));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(mediocre2_struct)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
struct S { C c; uint[] x; }
|
||||||
|
function f(uint a, S[2] s1, uint b) public returns (uint r1, C r2, uint r3) {
|
||||||
|
r1 = a;
|
||||||
|
r2 = s1[0].c;
|
||||||
|
r3 = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode, 0, "C");
|
||||||
|
string sig = "f(uint256,(address,uint256[])[2],uint256)";
|
||||||
|
ABI_CHECK(callContractFunction(sig,
|
||||||
|
7, 0x60, 8,
|
||||||
|
0x40, 7 * 0x20,
|
||||||
|
u256(u160(m_contractAddress)), 0x40,
|
||||||
|
2, 0x11, 0x12,
|
||||||
|
0x99, 0x40,
|
||||||
|
4, 0x31, 0x32, 0x34, 0x35
|
||||||
|
), encodeArgs(7, u256(u160(m_contractAddress)), 8));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(complex_struct)
|
||||||
|
{
|
||||||
|
string sourceCode = R"(
|
||||||
|
contract C {
|
||||||
|
enum E {A, B, C}
|
||||||
|
struct T { uint x; E e; uint8 y; }
|
||||||
|
struct S { C c; T[] t;}
|
||||||
|
function f(uint a, S[2] s1, S[] s2, uint b) public returns
|
||||||
|
(uint r1, C r2, uint r3, uint r4, C r5, uint r6, E r7, uint8 r8) {
|
||||||
|
r1 = a;
|
||||||
|
r2 = s1[0].c;
|
||||||
|
r3 = b;
|
||||||
|
r4 = s2.length;
|
||||||
|
r5 = s2[1].c;
|
||||||
|
r6 = s2[1].t.length;
|
||||||
|
r7 = s2[1].t[1].e;
|
||||||
|
r8 = s2[1].t[1].y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
NEW_ENCODER(
|
||||||
|
compileAndRun(sourceCode, 0, "C");
|
||||||
|
string sig = "f(uint256,(address,(uint256,uint8,uint8)[])[2],(address,(uint256,uint8,uint8)[])[],uint256)";
|
||||||
|
bytes args = encodeArgs(
|
||||||
|
7, 0x80, 0x1e0, 8,
|
||||||
|
// S[2] s1
|
||||||
|
0x40,
|
||||||
|
0x100,
|
||||||
|
// S s1[0]
|
||||||
|
u256(u160(m_contractAddress)),
|
||||||
|
0x40,
|
||||||
|
// T s1[0].t
|
||||||
|
1, // length
|
||||||
|
// s1[0].t[0]
|
||||||
|
0x11, 1, 0x12,
|
||||||
|
// S s1[1]
|
||||||
|
0, 0x40,
|
||||||
|
// T s1[1].t
|
||||||
|
0,
|
||||||
|
// S[] s2 (0x1e0)
|
||||||
|
2, // length
|
||||||
|
0x40, 0xa0,
|
||||||
|
// S s2[0]
|
||||||
|
0, 0x40, 0,
|
||||||
|
// S s2[1]
|
||||||
|
0x1234, 0x40,
|
||||||
|
// s2[1].t
|
||||||
|
3, // length
|
||||||
|
0, 0, 0,
|
||||||
|
0x21, 2, 0x22,
|
||||||
|
0, 0, 0
|
||||||
|
);
|
||||||
|
ABI_CHECK(callContractFunction(sig, args), encodeArgs(7, u256(u160(m_contractAddress)), 8, 2, 0x1234, 3, 2, 0x22));
|
||||||
|
// invalid enum value
|
||||||
|
args.data()[0x20 * 28] = 3;
|
||||||
|
ABI_CHECK(callContractFunction(sig, args), encodeArgs());
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_SUITE_END()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // end namespaces
|
@ -25,6 +25,8 @@
|
|||||||
#include <libsolidity/interface/Exceptions.h>
|
#include <libsolidity/interface/Exceptions.h>
|
||||||
#include <test/libsolidity/SolidityExecutionFramework.h>
|
#include <test/libsolidity/SolidityExecutionFramework.h>
|
||||||
|
|
||||||
|
#include <test/libsolidity/ABITestsCommon.h>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
using namespace dev::test;
|
using namespace dev::test;
|
||||||
@ -42,20 +44,6 @@ namespace test
|
|||||||
BOOST_CHECK_EQUAL(toHex(m_logs[0].data), toHex(DATA)); \
|
BOOST_CHECK_EQUAL(toHex(m_logs[0].data), toHex(DATA)); \
|
||||||
} while (false)
|
} while (false)
|
||||||
|
|
||||||
static string const NewEncoderPragma = "pragma experimental ABIEncoderV2;\n";
|
|
||||||
|
|
||||||
#define NEW_ENCODER(CODE) \
|
|
||||||
{ \
|
|
||||||
sourceCode = NewEncoderPragma + sourceCode; \
|
|
||||||
{ CODE } \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define BOTH_ENCODERS(CODE) \
|
|
||||||
{ \
|
|
||||||
{ CODE } \
|
|
||||||
NEW_ENCODER(CODE) \
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOST_FIXTURE_TEST_SUITE(ABIEncoderTest, SolidityExecutionFramework)
|
BOOST_FIXTURE_TEST_SUITE(ABIEncoderTest, SolidityExecutionFramework)
|
||||||
|
|
||||||
BOOST_AUTO_TEST_CASE(both_encoders_macro)
|
BOOST_AUTO_TEST_CASE(both_encoders_macro)
|
||||||
@ -74,7 +62,7 @@ BOOST_AUTO_TEST_CASE(value_types)
|
|||||||
string sourceCode = R"(
|
string sourceCode = R"(
|
||||||
contract C {
|
contract C {
|
||||||
event E(uint a, uint16 b, uint24 c, int24 d, bytes3 x, bool, C);
|
event E(uint a, uint16 b, uint24 c, int24 d, bytes3 x, bool, C);
|
||||||
function f() {
|
function f() public {
|
||||||
bytes6 x = hex"1bababababa2";
|
bytes6 x = hex"1bababababa2";
|
||||||
bool b;
|
bool b;
|
||||||
assembly { b := 7 }
|
assembly { b := 7 }
|
||||||
@ -98,7 +86,7 @@ BOOST_AUTO_TEST_CASE(string_literal)
|
|||||||
string sourceCode = R"(
|
string sourceCode = R"(
|
||||||
contract C {
|
contract C {
|
||||||
event E(string, bytes20, string);
|
event E(string, bytes20, string);
|
||||||
function f() {
|
function f() public {
|
||||||
E("abcdef", "abcde", "abcdefabcdefgehabcabcasdfjklabcdefabcedefghabcabcasdfjklabcdefabcdefghabcabcasdfjklabcdeefabcdefghabcabcasdefjklabcdefabcdefghabcabcasdfjkl");
|
E("abcdef", "abcde", "abcdefabcdefgehabcabcasdfjklabcdefabcedefghabcabcasdfjklabcdefabcdefghabcabcasdfjklabcdeefabcdefghabcabcasdefjklabcdefabcdefghabcabcasdfjkl");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,7 +108,7 @@ BOOST_AUTO_TEST_CASE(enum_type_cleanup)
|
|||||||
string sourceCode = R"(
|
string sourceCode = R"(
|
||||||
contract C {
|
contract C {
|
||||||
enum E { A, B }
|
enum E { A, B }
|
||||||
function f(uint x) returns (E en) {
|
function f(uint x) public returns (E en) {
|
||||||
assembly { en := x }
|
assembly { en := x }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,7 +126,7 @@ BOOST_AUTO_TEST_CASE(conversion)
|
|||||||
string sourceCode = R"(
|
string sourceCode = R"(
|
||||||
contract C {
|
contract C {
|
||||||
event E(bytes4, bytes4, uint16, uint8, int16, int8);
|
event E(bytes4, bytes4, uint16, uint8, int16, int8);
|
||||||
function f() {
|
function f() public {
|
||||||
bytes2 x; assembly { x := 0xf1f2f3f400000000000000000000000000000000000000000000000000000000 }
|
bytes2 x; assembly { x := 0xf1f2f3f400000000000000000000000000000000000000000000000000000000 }
|
||||||
uint8 a;
|
uint8 a;
|
||||||
uint16 b = 0x1ff;
|
uint16 b = 0x1ff;
|
||||||
@ -164,7 +152,7 @@ BOOST_AUTO_TEST_CASE(memory_array_one_dim)
|
|||||||
string sourceCode = R"(
|
string sourceCode = R"(
|
||||||
contract C {
|
contract C {
|
||||||
event E(uint a, int16[] b, uint c);
|
event E(uint a, int16[] b, uint c);
|
||||||
function f() {
|
function f() public {
|
||||||
int16[] memory x = new int16[](3);
|
int16[] memory x = new int16[](3);
|
||||||
assembly {
|
assembly {
|
||||||
for { let i := 0 } lt(i, 3) { i := add(i, 1) } {
|
for { let i := 0 } lt(i, 3) { i := add(i, 1) } {
|
||||||
@ -191,7 +179,7 @@ BOOST_AUTO_TEST_CASE(memory_array_two_dim)
|
|||||||
string sourceCode = R"(
|
string sourceCode = R"(
|
||||||
contract C {
|
contract C {
|
||||||
event E(uint a, int16[][2] b, uint c);
|
event E(uint a, int16[][2] b, uint c);
|
||||||
function f() {
|
function f() public {
|
||||||
int16[][2] memory x;
|
int16[][2] memory x;
|
||||||
x[0] = new int16[](3);
|
x[0] = new int16[](3);
|
||||||
x[1] = new int16[](2);
|
x[1] = new int16[](2);
|
||||||
@ -216,7 +204,7 @@ BOOST_AUTO_TEST_CASE(memory_byte_array)
|
|||||||
string sourceCode = R"(
|
string sourceCode = R"(
|
||||||
contract C {
|
contract C {
|
||||||
event E(uint a, bytes[] b, uint c);
|
event E(uint a, bytes[] b, uint c);
|
||||||
function f() {
|
function f() public {
|
||||||
bytes[] memory x = new bytes[](2);
|
bytes[] memory x = new bytes[](2);
|
||||||
x[0] = "abcabcdefghjklmnopqrsuvwabcdefgijklmnopqrstuwabcdefgijklmnoprstuvw";
|
x[0] = "abcabcdefghjklmnopqrsuvwabcdefgijklmnopqrstuwabcdefgijklmnoprstuvw";
|
||||||
x[1] = "abcdefghijklmnopqrtuvwabcfghijklmnopqstuvwabcdeghijklmopqrstuvw";
|
x[1] = "abcdefghijklmnopqrtuvwabcfghijklmnopqstuvwabcdeghijklmopqrstuvw";
|
||||||
@ -243,7 +231,7 @@ BOOST_AUTO_TEST_CASE(storage_byte_array)
|
|||||||
bytes short;
|
bytes short;
|
||||||
bytes long;
|
bytes long;
|
||||||
event E(bytes s, bytes l);
|
event E(bytes s, bytes l);
|
||||||
function f() {
|
function f() public {
|
||||||
short = "123456789012345678901234567890a";
|
short = "123456789012345678901234567890a";
|
||||||
long = "ffff123456789012345678901234567890afffffffff123456789012345678901234567890a";
|
long = "ffff123456789012345678901234567890afffffffff123456789012345678901234567890a";
|
||||||
E(short, long);
|
E(short, long);
|
||||||
@ -267,7 +255,7 @@ BOOST_AUTO_TEST_CASE(storage_array)
|
|||||||
contract C {
|
contract C {
|
||||||
address[3] addr;
|
address[3] addr;
|
||||||
event E(address[3] a);
|
event E(address[3] a);
|
||||||
function f() {
|
function f() public {
|
||||||
assembly {
|
assembly {
|
||||||
sstore(0, sub(0, 1))
|
sstore(0, sub(0, 1))
|
||||||
sstore(1, sub(0, 2))
|
sstore(1, sub(0, 2))
|
||||||
@ -290,7 +278,7 @@ BOOST_AUTO_TEST_CASE(storage_array_dyn)
|
|||||||
contract C {
|
contract C {
|
||||||
address[] addr;
|
address[] addr;
|
||||||
event E(address[] a);
|
event E(address[] a);
|
||||||
function f() {
|
function f() public {
|
||||||
addr.push(1);
|
addr.push(1);
|
||||||
addr.push(2);
|
addr.push(2);
|
||||||
addr.push(3);
|
addr.push(3);
|
||||||
@ -311,7 +299,7 @@ BOOST_AUTO_TEST_CASE(storage_array_compact)
|
|||||||
contract C {
|
contract C {
|
||||||
int72[] x;
|
int72[] x;
|
||||||
event E(int72[]);
|
event E(int72[]);
|
||||||
function f() {
|
function f() public {
|
||||||
x.push(-1);
|
x.push(-1);
|
||||||
x.push(2);
|
x.push(2);
|
||||||
x.push(-3);
|
x.push(-3);
|
||||||
@ -339,7 +327,7 @@ BOOST_AUTO_TEST_CASE(external_function)
|
|||||||
contract C {
|
contract C {
|
||||||
event E(function(uint) external returns (uint), function(uint) external returns (uint));
|
event E(function(uint) external returns (uint), function(uint) external returns (uint));
|
||||||
function(uint) external returns (uint) g;
|
function(uint) external returns (uint) g;
|
||||||
function f(uint) returns (uint) {
|
function f(uint) public returns (uint) {
|
||||||
g = this.f;
|
g = this.f;
|
||||||
E(this.f, g);
|
E(this.f, g);
|
||||||
}
|
}
|
||||||
@ -347,7 +335,7 @@ BOOST_AUTO_TEST_CASE(external_function)
|
|||||||
)";
|
)";
|
||||||
BOTH_ENCODERS(
|
BOTH_ENCODERS(
|
||||||
compileAndRun(sourceCode);
|
compileAndRun(sourceCode);
|
||||||
callContractFunction("f(uint256)");
|
callContractFunction("f(uint256)", u256(0));
|
||||||
string functionIdF = asString(m_contractAddress.ref()) + asString(FixedHash<4>(dev::keccak256("f(uint256)")).ref());
|
string functionIdF = asString(m_contractAddress.ref()) + asString(FixedHash<4>(dev::keccak256("f(uint256)")).ref());
|
||||||
REQUIRE_LOG_DATA(encodeArgs(functionIdF, functionIdF));
|
REQUIRE_LOG_DATA(encodeArgs(functionIdF, functionIdF));
|
||||||
)
|
)
|
||||||
@ -360,7 +348,7 @@ BOOST_AUTO_TEST_CASE(external_function_cleanup)
|
|||||||
event E(function(uint) external returns (uint), function(uint) external returns (uint));
|
event E(function(uint) external returns (uint), function(uint) external returns (uint));
|
||||||
// This test relies on the fact that g is stored in slot zero.
|
// This test relies on the fact that g is stored in slot zero.
|
||||||
function(uint) external returns (uint) g;
|
function(uint) external returns (uint) g;
|
||||||
function f(uint) returns (uint) {
|
function f(uint) public returns (uint) {
|
||||||
function(uint) external returns (uint)[1] memory h;
|
function(uint) external returns (uint)[1] memory h;
|
||||||
assembly { sstore(0, sub(0, 1)) mstore(h, sub(0, 1)) }
|
assembly { sstore(0, sub(0, 1)) mstore(h, sub(0, 1)) }
|
||||||
E(h[0], g);
|
E(h[0], g);
|
||||||
@ -369,7 +357,7 @@ BOOST_AUTO_TEST_CASE(external_function_cleanup)
|
|||||||
)";
|
)";
|
||||||
BOTH_ENCODERS(
|
BOTH_ENCODERS(
|
||||||
compileAndRun(sourceCode);
|
compileAndRun(sourceCode);
|
||||||
callContractFunction("f(uint256)");
|
callContractFunction("f(uint256)", u256(0));
|
||||||
REQUIRE_LOG_DATA(encodeArgs(string(24, char(-1)), string(24, char(-1))));
|
REQUIRE_LOG_DATA(encodeArgs(string(24, char(-1)), string(24, char(-1))));
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -404,7 +392,7 @@ BOOST_AUTO_TEST_CASE(function_name_collision)
|
|||||||
// and by the ABI encoder
|
// and by the ABI encoder
|
||||||
string sourceCode = R"(
|
string sourceCode = R"(
|
||||||
contract C {
|
contract C {
|
||||||
function f(uint x) returns (uint) {
|
function f(uint x) public returns (uint) {
|
||||||
assembly {
|
assembly {
|
||||||
function abi_encode_t_uint256_to_t_uint256() {
|
function abi_encode_t_uint256_to_t_uint256() {
|
||||||
mstore(0, 7)
|
mstore(0, 7)
|
||||||
@ -432,7 +420,7 @@ BOOST_AUTO_TEST_CASE(structs)
|
|||||||
struct T { uint64[2] x; }
|
struct T { uint64[2] x; }
|
||||||
S s;
|
S s;
|
||||||
event e(uint16, S);
|
event e(uint16, S);
|
||||||
function f() returns (uint, S) {
|
function f() public returns (uint, S) {
|
||||||
uint16 x = 7;
|
uint16 x = 7;
|
||||||
s.a = 8;
|
s.a = 8;
|
||||||
s.b = 9;
|
s.b = 9;
|
||||||
|
43
test/libsolidity/ABITestsCommon.h
Normal file
43
test/libsolidity/ABITestsCommon.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
This file is part of solidity.
|
||||||
|
|
||||||
|
solidity is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
solidity is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with solidity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace dev
|
||||||
|
{
|
||||||
|
namespace solidity
|
||||||
|
{
|
||||||
|
namespace test
|
||||||
|
{
|
||||||
|
|
||||||
|
static std::string const NewEncoderPragma = "pragma experimental ABIEncoderV2;\n";
|
||||||
|
|
||||||
|
#define NEW_ENCODER(CODE) \
|
||||||
|
{ \
|
||||||
|
sourceCode = NewEncoderPragma + sourceCode; \
|
||||||
|
{ CODE } \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define BOTH_ENCODERS(CODE) \
|
||||||
|
{ \
|
||||||
|
{ CODE } \
|
||||||
|
NEW_ENCODER(CODE) \
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // end namespaces
|
Loading…
Reference in New Issue
Block a user