/*
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 .
*/
// SPDX-License-Identifier: GPL-3.0
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
using namespace solidity;
using namespace solidity::yul;
using namespace solidity::util;
using namespace solidity::evmasm;
using namespace solidity::smtutil;
namespace
{
/// A helper class that would replace `mstore(key, expression)` for all `key` in
/// `m_keys`, by `pop(expression)`.
class EraseMemoryStores: public ASTModifier
{
public:
EraseMemoryStores(Block& _ast, set const& _keys, Dialect const& _dialect):
m_keys(_keys),
m_dialect(_dialect)
{
(*this)(_ast);
}
private:
using ASTModifier::operator();
void operator()(FunctionCall& _functionCall) override;
set const& m_keys;
Dialect const& m_dialect;
};
void EraseMemoryStores::operator()(FunctionCall& _functionCall)
{
ASTModifier::operator()(_functionCall);
if (_functionCall.functionName.name == m_dialect.memoryStoreFunction({})->name)
if (Identifier const* identifier = get_if(&_functionCall.arguments.at(0)))
if (m_keys.count(identifier->name))
{
BuiltinFunction const* discard = m_dialect.discardFunction({});
yulAssert(discard, "");
FunctionCall popFunctionCall{
_functionCall.location,
Identifier{_functionCall.location, discard->name},
{move(_functionCall.arguments.at(1))}
};
swap(_functionCall, popFunctionCall);
}
}
}
// Currently only defines simple upper bounds for some builtins.
smtutil::Expression MemoryStoreRemover::encodeEVMBuiltin(
evmasm::Instruction _instruction,
std::vector const& _arguments
)
{
vector arguments = applyMap(
_arguments,
[this](yul::Expression const& _expr) { return encodeExpression(_expr); }
);
switch (_instruction)
{
// Restrictions from EIP-1985
case evmasm::Instruction::CALLDATASIZE:
case evmasm::Instruction::CODESIZE:
case evmasm::Instruction::EXTCODESIZE:
case evmasm::Instruction::MSIZE:
case evmasm::Instruction::RETURNDATASIZE:
return newRestrictedVariable(bigint(1) << 32);
break;
default:
break;
}
return newRestrictedVariable();
}
void MemoryStoreRemover::operator()(FunctionCall const& _functionCall)
{
ASTWalker::operator()(_functionCall);
// Tracks two different things:
//
// 1. Tracking all keys for `mstore(key, value)`. Provided that `key` is is an SSA Variable.
// Notice that assigning to location `[key, key + 32)` multiple times is not an issue.
// Also, we have no conditions on `value`.
//
// 2. We keep track of every builtin-function call that reads from memory. This way, we don't
// have to deal with specialized logic for reads inside function calls or control flow.
if (_functionCall.functionName.name == m_dialect.memoryStoreFunction({})->name)
{
yulAssert(_functionCall.arguments.size() == 2, "");
if (Identifier const* identifier = get_if(&_functionCall.arguments.at(0)))
if (m_ssaVariables.count(identifier->name))
m_memoryKeys.insert(identifier->name);
}
else if (auto instruction = toEVMInstruction(m_dialect, _functionCall.functionName.name))
if (SemanticInformation::readsMemory(*instruction))
m_memoryReads.emplace_back(&_functionCall);
}
void MemoryStoreRemover::operator()(VariableDeclaration const& _varDecl)
{
ASTWalker::operator()(_varDecl);
encodeVariableDeclaration(_varDecl);
}
void MemoryStoreRemover::encodeMemoryRead(
FunctionCall const& _functionCall,
smtutil::Expression _memoryLocation
)
{
vector arguments = applyMap(
_functionCall.arguments,
[this](yul::Expression const& _expr) { return encodeExpression(_expr); }
);
optional instruction = toEVMInstruction(m_dialect, _functionCall.functionName.name);
yulAssert(instruction.has_value(), "A function call that was not an EVM builtin.");
yulAssert(SemanticInformation::readsMemory(*instruction), "");
// Encode read to memory location [p, p + n)
auto encodeRead = [&](auto const& p, auto const& n)
{
// Check if p + n can overflow.
// EIP 1985 restricts the value of `msize` to be 2**32. Therefore, in practice `p + n`
// cannot overflow. But assuming this can lead to incorrectly removing `mstore`, even though
// such a contract cannot be executed in the first place because of gas reasons.
m_solver->push();
m_solver->addAssertion(p + n >= constantValue(bigint(1) << 256));
auto result = m_solver->check({});
m_solver->pop();
// By skipping the encoding, `m_solver.check({})` inside `run(...)` will be satisfiable.
if (result.first == CheckResult::UNSATISFIABLE)
{
m_solver->addAssertion(p <= _memoryLocation);
m_solver->addAssertion(_memoryLocation < p + n);
}
};
switch (*instruction)
{
case Instruction::CREATE:
yulAssert(arguments.size() == 3, "");
encodeRead(arguments.at(1), arguments.at(2));
break;
case Instruction::CREATE2:
yulAssert(arguments.size() == 4, "");
encodeRead(arguments.at(1), arguments.at(2));
break;
case Instruction::KECCAK256:
case Instruction::RETURN:
case Instruction::REVERT:
yulAssert(arguments.size() == 2, "");
encodeRead(arguments.at(0), arguments.at(1));
break;
case Instruction::MLOAD:
yulAssert(arguments.size() == 1, "");
encodeRead(arguments.at(0), constantValue(32));
break;
case Instruction::MSIZE:
yulAssert(false, "The ast cannot contain msize.");
break;
case Instruction::LOG0:
yulAssert(arguments.size() == 2, "");
encodeRead(arguments.at(0), arguments.at(1));
break;
case Instruction::LOG1:
yulAssert(arguments.size() == 3, "");
encodeRead(arguments.at(0), arguments.at(1));
break;
case Instruction::LOG2:
yulAssert(arguments.size() == 4, "");
encodeRead(arguments.at(0), arguments.at(1));
break;
case Instruction::LOG3:
yulAssert(arguments.size() == 5, "");
encodeRead(arguments.at(0), arguments.at(1));
break;
case Instruction::LOG4:
yulAssert(arguments.size() == 6, "");
encodeRead(arguments.at(0), arguments.at(1));
break;
// Instructions that read and write from memory
case Instruction::CODECOPY:
yulAssert(arguments.size() == 3, "");
encodeRead(arguments.at(1), arguments.at(2));
break;
case Instruction::CALL:
case Instruction::CALLCODE:
case Instruction::DELEGATECALL:
yulAssert(arguments.size() == 7, "");
encodeRead(arguments.at(3), arguments.at(4));
break;
case Instruction::STATICCALL:
yulAssert(arguments.size() == 6, "");
encodeRead(arguments.at(2), arguments.at(3));
break;
default:
yulAssert(false, "");
break;
}
}
void MemoryStoreRemover::run(OptimiserStepContext& _context, Block& _ast)
{
if (dynamic_cast(&_context.dialect))
return;
// We skip because the step is not guaranteed to preserve semantics of `msize`.
if (MSizeFinder::containsMSize(_context.dialect, _ast))
return;
set ssaVariables = SSAValueTracker::ssaVariables(_ast);
MemoryStoreRemover m{ssaVariables, _context.dialect};
m(_ast);
set memoryKeysToErase = move(m.m_memoryKeys);
// A dummy variable, called `x`, that would be repeated during computations
smtutil::Expression memoryLocation = m.newVariable();
// For each memory location, say `key`, we try to prove that the memory hasn't been read from.
// This is equivalent to showing the following:
// For every builtin call that reads from `[a, b)`,
// the system of equations:
//
// a <= x < b
// key <= x < key + 32
//
// is infeasible. Here `x` is the dummy variable defined above.
// The loop tries to check if such a system if not-infeasible.
// If it's not-infeasible, then, the location cannot be removed.
cxx20::erase_if(memoryKeysToErase, [&](auto const& _key)
{
// We check if `key + 32` can overflow. If possible, we cannot erase `mstore(key, ...)`
auto keyAsExpr = m.m_variables.at(_key);
m.m_solver->push();
m.m_solver->addAssertion(
keyAsExpr + m.constantValue(32) >= m.constantValue(bigint(1) << 256)
);
auto result1 = m.m_solver->check({});
m.m_solver->pop();
if (result1.first != CheckResult::UNSATISFIABLE)
return true;
for (auto const functionCall: m.m_memoryReads)
{
yulAssert(functionCall, "");
m.m_solver->push();
m.encodeMemoryRead(*functionCall, memoryLocation);
m.m_solver->addAssertion(keyAsExpr <= memoryLocation);
m.m_solver->addAssertion(memoryLocation < keyAsExpr + m.constantValue(32));
auto result2 = m.m_solver->check({});
m.m_solver->pop();
// Notice that we check for not unsatisfiable, instead of == satisfiable.
// Therefore, other SMT results such as `UNKNOWN`, `ERROR`, etc., would not set up the
// key for erasing.
if (result2.first != CheckResult::UNSATISFIABLE)
return true;
}
return false;
});
EraseMemoryStores{_ast, memoryKeysToErase, _context.dialect};
}