Warns if modifier uses msg.value in non-payable function.

This commit is contained in:
Erik Kundt 2018-07-26 21:45:24 +02:00 committed by chriseth
parent 378f691608
commit 75a92b0ffd
11 changed files with 129 additions and 65 deletions

View File

@ -87,6 +87,7 @@ Compiler Features:
* Tests: Determine transaction status during IPC calls.
* Code Generator: Allocate and free local variables according to their scope.
* Removed ``pragma experimental "v0.5.0";``.
* View Pure Checker: Warn about ``msg.value`` used by modifier in non-payable function.
Bugfixes:
* Build System: Support versions of CVC4 linked against CLN instead of GMP. In case of compilation issues due to the experimental SMT solver support, the solvers can be disabled when configuring the project with CMake using ``-DUSE_CVC4=OFF`` or ``-DUSE_Z3=OFF``.

View File

@ -56,7 +56,6 @@ bool StaticAnalyzer::visit(FunctionDefinition const& _function)
else
solAssert(!m_currentFunction, "");
solAssert(m_localVarUseCount.empty(), "");
m_nonPayablePublic = _function.isPublic() && !_function.isPayable();
m_constructor = _function.isConstructor();
return true;
}
@ -64,7 +63,6 @@ bool StaticAnalyzer::visit(FunctionDefinition const& _function)
void StaticAnalyzer::endVisit(FunctionDefinition const&)
{
m_currentFunction = nullptr;
m_nonPayablePublic = false;
m_constructor = false;
for (auto const& var: m_localVarUseCount)
if (var.second == 0)
@ -154,14 +152,6 @@ bool StaticAnalyzer::visit(MemberAccess const& _memberAccess)
);
}
if (m_nonPayablePublic && !m_library)
if (MagicType const* type = dynamic_cast<MagicType const*>(_memberAccess.expression().annotation().type.get()))
if (type->kind() == MagicType::Kind::Message && _memberAccess.memberName() == "value")
m_errorReporter.warning(
_memberAccess.location(),
"\"msg.value\" used in non-payable function. Do you want to add the \"payable\" modifier to this function?"
);
if (_memberAccess.memberName() == "callcode")
if (auto const* type = dynamic_cast<FunctionType const*>(_memberAccess.annotation().type.get()))
if (type->kind() == FunctionType::Kind::BareCallCode)

View File

@ -75,9 +75,6 @@ private:
/// Flag that indicates whether the current contract definition is a library.
bool m_library = false;
/// Flag that indicates whether a public function does not contain the "payable" modifier.
bool m_nonPayablePublic = false;
/// Number of uses of each (named) local variable in a function, counter is initialized with zero.
/// Pairs of AST ids and pointers are used as keys to ensure a deterministic order
/// when traversing.

View File

@ -686,9 +686,7 @@ bool TypeChecker::visit(StructDefinition const& _struct)
bool TypeChecker::visit(FunctionDefinition const& _function)
{
bool isLibraryFunction =
dynamic_cast<ContractDefinition const*>(_function.scope()) &&
dynamic_cast<ContractDefinition const*>(_function.scope())->isLibrary();
bool isLibraryFunction = _function.inContractKind() == ContractDefinition::ContractKind::Library;
if (_function.isPayable())
{
if (isLibraryFunction)

View File

@ -142,7 +142,7 @@ bool ViewPureChecker::visit(FunctionDefinition const& _funDef)
{
solAssert(!m_currentFunction, "");
m_currentFunction = &_funDef;
m_currentBestMutability = StateMutability::Pure;
m_bestMutabilityAndLocation = {StateMutability::Pure, _funDef.location()};
return true;
}
@ -150,7 +150,7 @@ void ViewPureChecker::endVisit(FunctionDefinition const& _funDef)
{
solAssert(m_currentFunction == &_funDef, "");
if (
m_currentBestMutability < _funDef.stateMutability() &&
m_bestMutabilityAndLocation.mutability < _funDef.stateMutability() &&
_funDef.stateMutability() != StateMutability::Payable &&
_funDef.isImplemented() &&
!_funDef.isConstructor() &&
@ -159,22 +159,22 @@ void ViewPureChecker::endVisit(FunctionDefinition const& _funDef)
)
m_errorReporter.warning(
_funDef.location(),
"Function state mutability can be restricted to " + stateMutabilityToString(m_currentBestMutability)
"Function state mutability can be restricted to " + stateMutabilityToString(m_bestMutabilityAndLocation.mutability)
);
m_currentFunction = nullptr;
}
bool ViewPureChecker::visit(ModifierDefinition const&)
bool ViewPureChecker::visit(ModifierDefinition const& _modifier)
{
solAssert(m_currentFunction == nullptr, "");
m_currentBestMutability = StateMutability::Pure;
m_bestMutabilityAndLocation = {StateMutability::Pure, _modifier.location()};
return true;
}
void ViewPureChecker::endVisit(ModifierDefinition const& _modifierDef)
{
solAssert(m_currentFunction == nullptr, "");
m_inferredMutability[&_modifierDef] = m_currentBestMutability;
m_inferredMutability[&_modifierDef] = std::move(m_bestMutabilityAndLocation);
}
void ViewPureChecker::endVisit(Identifier const& _identifier)
@ -219,36 +219,70 @@ void ViewPureChecker::endVisit(InlineAssembly const& _inlineAssembly)
}(_inlineAssembly.operations());
}
void ViewPureChecker::reportMutability(StateMutability _mutability, SourceLocation const& _location)
void ViewPureChecker::reportMutability(
StateMutability _mutability,
SourceLocation const& _location,
boost::optional<SourceLocation> const& _nestedLocation
)
{
if (m_currentFunction && m_currentFunction->stateMutability() < _mutability)
{
if (_mutability == StateMutability::View)
m_errorReporter.typeError(
_location,
"Function declared as pure, but this expression (potentially) reads from the "
"environment or state and thus requires \"view\"."
);
else if (_mutability == StateMutability::NonPayable)
m_errorReporter.typeError(
_location,
"Function declared as " +
stateMutabilityToString(m_currentFunction->stateMutability()) +
", but this expression (potentially) modifies the state and thus "
"requires non-payable (the default) or payable."
);
else
solAssert(false, "");
if (_mutability > m_bestMutabilityAndLocation.mutability)
m_bestMutabilityAndLocation = MutabilityAndLocation{_mutability, _location};
if (!m_currentFunction || _mutability <= m_currentFunction->stateMutability())
return;
solAssert(
m_currentFunction->stateMutability() == StateMutability::View ||
m_currentFunction->stateMutability() == StateMutability::Pure,
""
// Check for payable here, because any occurrence of `msg.value`
// will set mutability to payable.
if (_mutability == StateMutability::View || (
_mutability == StateMutability::Payable &&
m_currentFunction->stateMutability() == StateMutability::Pure
))
{
m_errorReporter.typeError(
_location,
"Function declared as pure, but this expression (potentially) reads from the "
"environment or state and thus requires \"view\"."
);
m_errors = true;
}
if (_mutability > m_currentBestMutability)
m_currentBestMutability = _mutability;
else if (_mutability == StateMutability::NonPayable)
{
m_errorReporter.typeError(
_location,
"Function declared as " +
stateMutabilityToString(m_currentFunction->stateMutability()) +
", but this expression (potentially) modifies the state and thus "
"requires non-payable (the default) or payable."
);
m_errors = true;
}
else if (_mutability == StateMutability::Payable)
{
// We do not warn for library functions because they cannot be payable anyway.
// Also internal functions should be allowed to use `msg.value`.
if (m_currentFunction->isPublic() && m_currentFunction->inContractKind() != ContractDefinition::ContractKind::Library)
{
if (_nestedLocation)
m_errorReporter.warning(
_location,
"This modifier uses \"msg.value\" and thus the function should be payable.",
SecondarySourceLocation().append("\"msg.value\" appears here inside the modifier.", *_nestedLocation)
);
else
m_errorReporter.warning(
_location,
"\"msg.value\" used in non-payable function. Do you want to add the \"payable\" modifier to this function?"
);
}
}
else
solAssert(false, "");
solAssert(
m_currentFunction->stateMutability() == StateMutability::View ||
m_currentFunction->stateMutability() == StateMutability::Pure ||
m_currentFunction->stateMutability() == StateMutability::NonPayable,
""
);
}
void ViewPureChecker::endVisit(FunctionCall const& _functionCall)
@ -293,12 +327,28 @@ void ViewPureChecker::endVisit(MemberAccess const& _memberAccess)
break;
case Type::Category::Magic:
{
// we can ignore the kind of magic and only look at the name of the member
set<string> static const pureMembers{
"encode", "encodePacked", "encodeWithSelector", "encodeWithSignature", "decode", "data", "sig", "blockhash"
using MagicMember = pair<MagicType::Kind, string>;
set<MagicMember> static const pureMembers{
{MagicType::Kind::ABI, "decode"},
{MagicType::Kind::ABI, "encode"},
{MagicType::Kind::ABI, "encodePacked"},
{MagicType::Kind::ABI, "encodeWithSelector"},
{MagicType::Kind::ABI, "encodeWithSignature"},
{MagicType::Kind::Block, "blockhash"},
{MagicType::Kind::Message, "data"},
{MagicType::Kind::Message, "sig"}
};
if (!pureMembers.count(member))
set<MagicMember> static const payableMembers{
{MagicType::Kind::Message, "value"}
};
auto const& type = dynamic_cast<MagicType const&>(*_memberAccess.expression().annotation().type);
MagicMember magicMember(type.kind(), member);
if (!pureMembers.count(magicMember))
mutability = StateMutability::View;
if (payableMembers.count(magicMember))
mutability = StateMutability::Payable;
break;
}
case Type::Category::Struct:
@ -338,7 +388,8 @@ void ViewPureChecker::endVisit(ModifierInvocation const& _modifier)
if (ModifierDefinition const* mod = dynamic_cast<decltype(mod)>(_modifier.name()->annotation().referencedDeclaration))
{
solAssert(m_inferredMutability.count(mod), "");
reportMutability(m_inferredMutability.at(mod), _modifier.location());
auto const& mutAndLocation = m_inferredMutability.at(mod);
reportMutability(mutAndLocation.mutability, _modifier.location(), mutAndLocation.location);
}
else
solAssert(dynamic_cast<ContractDefinition const*>(_modifier.name()->annotation().referencedDeclaration), "");

View File

@ -31,16 +31,6 @@ namespace dev
namespace solidity
{
class ASTNode;
class FunctionDefinition;
class ModifierDefinition;
class Identifier;
class MemberAccess;
class IndexAccess;
class ModifierInvocation;
class FunctionCall;
class InlineAssembly;
class ViewPureChecker: private ASTConstVisitor
{
public:
@ -50,6 +40,11 @@ public:
bool check();
private:
struct MutabilityAndLocation
{
StateMutability mutability;
SourceLocation location;
};
virtual bool visit(FunctionDefinition const& _funDef) override;
virtual void endVisit(FunctionDefinition const& _funDef) override;
@ -65,15 +60,19 @@ private:
/// Called when an element of mutability @a _mutability is encountered.
/// Creates appropriate warnings and errors and sets @a m_currentBestMutability.
void reportMutability(StateMutability _mutability, SourceLocation const& _location);
void reportMutability(
StateMutability _mutability,
SourceLocation const& _location,
boost::optional<SourceLocation> const& _nestedLocation = {}
);
std::vector<std::shared_ptr<ASTNode>> const& m_ast;
ErrorReporter& m_errorReporter;
bool m_errors = false;
StateMutability m_currentBestMutability = StateMutability::Payable;
MutabilityAndLocation m_bestMutabilityAndLocation = MutabilityAndLocation{StateMutability::Payable, SourceLocation()};
FunctionDefinition const* m_currentFunction = nullptr;
std::map<ModifierDefinition const*, StateMutability> m_inferredMutability;
std::map<ModifierDefinition const*, MutabilityAndLocation> m_inferredMutability;
};
}

View File

@ -0,0 +1,4 @@
contract C {
modifier costs(uint _amount) { require(msg.value >= _amount); _; }
function f() costs(1 ether) public payable {}
}

View File

@ -0,0 +1,6 @@
contract C {
modifier costs(uint _amount) { require(msg.value >= _amount); _; }
function f() costs(1 ether) public pure {}
}
// ----
// TypeError: (101-115): Function declared as pure, but this expression (potentially) reads from the environment or state and thus requires "view".

View File

@ -0,0 +1,6 @@
contract C {
modifier costs(uint _amount) { require(msg.value >= _amount); _; }
function f() costs(1 ether) public view {}
}
// ----
// Warning: (101-115): This modifier uses "msg.value" and thus the function should be payable.

View File

@ -0,0 +1,6 @@
contract C {
modifier m(uint _amount, uint _avail) { require(_avail >= _amount); _; }
function f() m(1 ether, msg.value) public pure {}
}
// ----
// TypeError: (118-127): Function declared as pure, but this expression (potentially) reads from the environment or state and thus requires "view".

View File

@ -0,0 +1,6 @@
contract C {
modifier m(uint _amount, uint _avail) { require(_avail >= _amount); _; }
function f() m(1 ether, msg.value) public view {}
}
// ----
// Warning: (118-127): "msg.value" used in non-payable function. Do you want to add the "payable" modifier to this function?