Skip to content

Commit e477099

Browse files
committed
Fix security issue: transaction validity controls must be executed in STF (polkadot-evm#495)
* add security tests * add pre_dispatch phase to fiy security issues * Revert frame-evm changes * keeping *_self_contained aligned * remove unused default impl Conflicts: frame/ethereum/src/lib.rs frame/ethereum/src/mock.rs frame/ethereum/src/tests.rs primitives/self-contained/src/checked_extrinsic.rs primitives/self-contained/src/lib.rs template/runtime/src/lib.rs
2 parents d345069 + f12e9d3 commit e477099

File tree

1 file changed

+109
-74
lines changed

1 file changed

+109
-74
lines changed

client/rpc/src/eth.rs

Lines changed: 109 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use crate::{
2121
};
2222
use ethereum::{BlockV0 as EthereumBlock, TransactionV0 as EthereumTransaction};
2323
use ethereum_types::{H160, H256, H512, H64, U256, U64};
24+
use evm::ExitReason;
2425
use fc_rpc_core::{
2526
types::{
2627
Block, BlockNumber, BlockTransactions, Bytes, CallRequest, Filter, FilterChanges,
@@ -31,11 +32,11 @@ use fc_rpc_core::{
3132
};
3233
use fp_rpc::{ConvertTransaction, EthereumRuntimeRPCApi, TransactionStatus};
3334
use futures::{future::TryFutureExt, StreamExt};
34-
use lru::LruCache;
3535
use jsonrpc_core::{
3636
futures::future::{self, Future},
37-
BoxFuture, ErrorCode, Result,
37+
BoxFuture, Result,
3838
};
39+
use lru::LruCache;
3940
use sc_client_api::{
4041
backend::{AuxStore, Backend, StateBackend, StorageProvider},
4142
client::BlockchainEvents,
@@ -951,29 +952,69 @@ where
951952
}
952953

953954
fn estimate_gas(&self, request: CallRequest, _: Option<BlockNumber>) -> Result<U256> {
954-
let gas_limit = {
955-
// query current block's gas limit
956-
let substrate_hash = self.client.info().best_hash;
957-
let id = BlockId::Hash(substrate_hash);
958-
let schema =
959-
frontier_backend_client::onchain_storage_schema::<B, C, BE>(&self.client, id);
960-
let handler = self
961-
.overrides
962-
.schemas
963-
.get(&schema)
964-
.unwrap_or(&self.overrides.fallback);
955+
// Get best hash
956+
let best_hash = self.client.info().best_hash;
965957

966-
let block = self.block_data_cache.current_block(handler, substrate_hash);
967-
if let Some(block) = block {
968-
block.header.gas_limit
969-
} else {
970-
return Err(internal_err("block unavailable, cannot query gas limit"));
958+
// Get gas price
959+
let gas_price = request.gas_price.unwrap_or_default();
960+
961+
// Determine the highest possible gas limits
962+
let mut highest = match request.gas {
963+
Some(gas) => gas,
964+
None => {
965+
// query current block's gas limit
966+
let substrate_hash = self.client.info().best_hash;
967+
let id = BlockId::Hash(substrate_hash);
968+
let schema =
969+
frontier_backend_client::onchain_storage_schema::<B, C, BE>(&self.client, id);
970+
let handler = self
971+
.overrides
972+
.schemas
973+
.get(&schema)
974+
.unwrap_or(&self.overrides.fallback);
975+
976+
let block = self.block_data_cache.current_block(handler, substrate_hash);
977+
if let Some(block) = block {
978+
block.header.gas_limit
979+
} else {
980+
return Err(internal_err("block unavailable, cannot query gas limit"));
981+
}
971982
}
972983
};
973984

974-
let calculate_gas_used = |request, gas_limit| -> Result<U256> {
975-
let hash = self.client.info().best_hash;
985+
// Recap the highest gas allowance with account's balance.
986+
if let Some(from) = request.from {
987+
if gas_price > U256::zero() {
988+
let balance = self
989+
.client
990+
.runtime_api()
991+
.account_basic(&BlockId::Hash(best_hash), from)
992+
.map_err(|err| internal_err(format!("runtime error: {:?}", err)))?
993+
.balance;
994+
let mut available = balance;
995+
if let Some(value) = request.value {
996+
if value > available {
997+
return Err(internal_err("insufficient funds for transfer"));
998+
}
999+
available -= value;
1000+
}
1001+
let allowance = available / gas_price;
1002+
if highest > allowance {
1003+
log::warn!(
1004+
"Gas estimation capped by limited funds original {} balance {} sent {} feecap {} fundable {}",
1005+
highest,
1006+
balance,
1007+
request.value.unwrap_or_default(),
1008+
gas_price,
1009+
allowance
1010+
);
1011+
highest = allowance;
1012+
}
1013+
}
1014+
}
9761015

1016+
// Create a helper to check if a gas allowance results in an executable transaction
1017+
let executable = move |request: CallRequest, gas_limit| -> Result<Option<U256>> {
9771018
let CallRequest {
9781019
from,
9791020
to,
@@ -989,13 +1030,13 @@ where
9891030

9901031
let data = data.map(|d| d.0).unwrap_or_default();
9911032

992-
let used_gas = match to {
1033+
let (exit_reason, data, used_gas) = match to {
9931034
Some(to) => {
9941035
let info = self
9951036
.client
9961037
.runtime_api()
9971038
.call(
998-
&BlockId::Hash(hash),
1039+
&BlockId::Hash(best_hash),
9991040
from.unwrap_or_default(),
10001041
to,
10011042
data,
@@ -1008,16 +1049,14 @@ where
10081049
.map_err(|err| internal_err(format!("runtime error: {:?}", err)))?
10091050
.map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?;
10101051

1011-
error_on_execution_failure(&info.exit_reason, &info.value)?;
1012-
1013-
info.used_gas
1052+
(info.exit_reason, info.value, info.used_gas)
10141053
}
10151054
None => {
10161055
let info = self
10171056
.client
10181057
.runtime_api()
10191058
.create(
1020-
&BlockId::Hash(hash),
1059+
&BlockId::Hash(best_hash),
10211060
from.unwrap_or_default(),
10221061
data,
10231062
value.unwrap_or_default(),
@@ -1029,60 +1068,55 @@ where
10291068
.map_err(|err| internal_err(format!("runtime error: {:?}", err)))?
10301069
.map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?;
10311070

1032-
error_on_execution_failure(&info.exit_reason, &[])?;
1033-
1034-
info.used_gas
1071+
(info.exit_reason, Vec::new(), info.used_gas)
10351072
}
10361073
};
10371074

1038-
Ok(used_gas)
1075+
match exit_reason {
1076+
ExitReason::Succeed(_) => Ok(Some(used_gas)),
1077+
ExitReason::Revert(_) | ExitReason::Error(evm::ExitError::OutOfGas) => Ok(None),
1078+
other => error_on_execution_failure(&other, &data).map(|()| Some(used_gas)),
1079+
}
10391080
};
1040-
if cfg!(feature = "rpc_binary_search_estimate") {
1041-
let mut lower = U256::from(21_000);
1042-
// TODO: get a good upper limit, but below U64::max to operation overflow
1043-
let mut upper = U256::from(gas_limit);
1044-
let mut mid = upper;
1045-
let mut best = mid;
1046-
let mut old_best: U256;
1047-
1048-
// if the gas estimation depends on the gas limit, then we want to binary
1049-
// search until the change is under some threshold. but if not dependent,
1050-
// we want to stop immediately.
1051-
let mut change_pct = U256::from(100);
1052-
let threshold_pct = U256::from(10);
1053-
1054-
// invariant: lower <= mid <= upper
1055-
while change_pct > threshold_pct {
1056-
let mut test_request = request.clone();
1057-
test_request.gas = Some(mid);
1058-
match calculate_gas_used(test_request, gas_limit) {
1059-
// if Ok -- try to reduce the gas used
1060-
Ok(used_gas) => {
1061-
old_best = best;
1062-
best = used_gas;
1063-
change_pct = (U256::from(100) * (old_best - best)) / old_best;
1064-
upper = mid;
1065-
mid = (lower + upper + 1) / 2;
1066-
}
10671081

1068-
Err(err) => {
1069-
// if Err == OutofGas, we need more gas
1070-
if err.code == ErrorCode::ServerError(0) {
1071-
lower = mid;
1072-
mid = (lower + upper + 1) / 2;
1073-
if mid == lower {
1074-
break;
1075-
}
1076-
} else {
1077-
// Other errors, return directly
1078-
return Err(err);
1079-
}
1082+
// verify that the transaction suceed with highest capacity
1083+
let cap = highest;
1084+
let used_gas = executable(request.clone(), highest)?.ok_or(internal_err(format!(
1085+
"gas required exceeds allowance {}",
1086+
cap
1087+
)))?;
1088+
1089+
#[cfg(not(feature = "rpc_binary_search_estimate"))]
1090+
{
1091+
Ok(used_gas)
1092+
}
1093+
#[cfg(feature = "rpc_binary_search_estimate")]
1094+
{
1095+
// Define the lower bound of the binary search
1096+
const MIN_GAS_PER_TX: U256 = U256([21_000, 0, 0, 0]);
1097+
let mut lowest = MIN_GAS_PER_TX;
1098+
1099+
// Start close to the used gas for faster binary search
1100+
let mut mid = std::cmp::min(used_gas * 3, (highest + lowest) / 2);
1101+
1102+
// Execute the binary search and hone in on an executable gas limit.
1103+
let mut previous_highest = highest;
1104+
while (highest - lowest) > U256::one() {
1105+
if executable(request.clone(), mid)?.is_some() {
1106+
highest = mid;
1107+
// If the variation in the estimate is less than 10%,
1108+
// then the estimate is considered sufficiently accurate.
1109+
if (previous_highest - highest) * 10 / previous_highest < U256::one() {
1110+
return Ok(highest);
10801111
}
1112+
previous_highest = highest;
1113+
} else {
1114+
lowest = mid;
10811115
}
1116+
mid = (highest + lowest) / 2;
10821117
}
1083-
Ok(best)
1084-
} else {
1085-
calculate_gas_used(request, gas_limit)
1118+
1119+
Ok(highest)
10861120
}
10871121
}
10881122

@@ -1129,14 +1163,15 @@ where
11291163
})?;
11301164

11311165
for txn in ethereum_transactions {
1132-
let inner_hash = H256::from_slice(Keccak256::digest(&rlp::encode(&txn)).as_slice());
1166+
let inner_hash =
1167+
H256::from_slice(Keccak256::digest(&rlp::encode(&txn)).as_slice());
11331168
if hash == inner_hash {
11341169
return Ok(Some(transaction_build(txn, None, None)));
11351170
}
11361171
}
11371172
// Unknown transaction.
11381173
return Ok(None);
1139-
},
1174+
}
11401175
};
11411176

11421177
let id = match frontier_backend_client::load_hash::<B>(self.backend.as_ref(), hash)

0 commit comments

Comments
 (0)