@@ -167,6 +167,7 @@ pub trait Ext: sealing::Sealed {
167
167
to : AccountIdOf < Self :: T > ,
168
168
value : BalanceOf < Self :: T > ,
169
169
input_data : Vec < u8 > ,
170
+ allows_reentry : bool ,
170
171
) -> Result < ( ExecReturnValue , u32 ) , ( ExecError , u32 ) > ;
171
172
172
173
/// Instantiate a contract from the given code.
@@ -457,6 +458,8 @@ pub struct Frame<T: Config> {
457
458
entry_point : ExportedFunction ,
458
459
/// The gas meter capped to the supplied gas limit.
459
460
nested_meter : GasMeter < T > ,
461
+ /// If `false` the contract enabled its defense against reentrance attacks.
462
+ allows_reentry : bool ,
460
463
}
461
464
462
465
/// Parameter passed in when creating a new `Frame`.
@@ -731,6 +734,7 @@ where
731
734
entry_point,
732
735
nested_meter : gas_meter. nested ( gas_limit)
733
736
. map_err ( |e| ( e. into ( ) , executable. code_len ( ) ) ) ?,
737
+ allows_reentry : true ,
734
738
} ;
735
739
736
740
Ok ( ( frame, executable) )
@@ -1014,6 +1018,11 @@ where
1014
1018
self . frames ( ) . skip ( 1 ) . any ( |f| & f. account_id == account_id)
1015
1019
}
1016
1020
1021
+ /// Returns whether the specified contract allows to be reentered right now.
1022
+ fn allows_reentry ( & self , id : & AccountIdOf < T > ) -> bool {
1023
+ !self . frames ( ) . any ( |f| & f. account_id == id && !f. allows_reentry )
1024
+ }
1025
+
1017
1026
/// Increments the cached account id and returns the value to be used for the trie_id.
1018
1027
fn next_trie_seed ( & mut self ) -> u64 {
1019
1028
let next = if let Some ( current) = self . account_counter {
@@ -1045,25 +1054,44 @@ where
1045
1054
to : T :: AccountId ,
1046
1055
value : BalanceOf < T > ,
1047
1056
input_data : Vec < u8 > ,
1057
+ allows_reentry : bool ,
1048
1058
) -> Result < ( ExecReturnValue , u32 ) , ( ExecError , u32 ) > {
1049
- // We ignore instantiate frames in our search for a cached contract.
1050
- // Otherwise it would be possible to recursively call a contract from its own
1051
- // constructor: We disallow calling not fully constructed contracts.
1052
- let cached_info = self
1053
- . frames ( )
1054
- . find ( |f| f. entry_point == ExportedFunction :: Call && f. account_id == to)
1055
- . and_then ( |f| {
1056
- match & f. contract_info {
1057
- CachedContract :: Cached ( contract) => Some ( contract. clone ( ) ) ,
1058
- _ => None ,
1059
- }
1060
- } ) ;
1061
- let executable = self . push_frame (
1062
- FrameArgs :: Call { dest : to, cached_info} ,
1063
- value,
1064
- gas_limit
1065
- ) ?;
1066
- self . run ( executable, input_data)
1059
+ // Before pushing the new frame: Protect the caller contract against reentrancy attacks.
1060
+ // It is important to do this before calling `allows_reentry` so that a direct recursion
1061
+ // is caught by it.
1062
+ self . top_frame_mut ( ) . allows_reentry = allows_reentry;
1063
+
1064
+ let try_call = || {
1065
+ if !self . allows_reentry ( & to) {
1066
+ return Err ( ( <Error < T > >:: ReentranceDenied . into ( ) , 0 ) ) ;
1067
+ }
1068
+ // We ignore instantiate frames in our search for a cached contract.
1069
+ // Otherwise it would be possible to recursively call a contract from its own
1070
+ // constructor: We disallow calling not fully constructed contracts.
1071
+ let cached_info = self
1072
+ . frames ( )
1073
+ . find ( |f| f. entry_point == ExportedFunction :: Call && f. account_id == to)
1074
+ . and_then ( |f| {
1075
+ match & f. contract_info {
1076
+ CachedContract :: Cached ( contract) => Some ( contract. clone ( ) ) ,
1077
+ _ => None ,
1078
+ }
1079
+ } ) ;
1080
+ let executable = self . push_frame (
1081
+ FrameArgs :: Call { dest : to, cached_info} ,
1082
+ value,
1083
+ gas_limit
1084
+ ) ?;
1085
+ self . run ( executable, input_data)
1086
+ } ;
1087
+
1088
+ // We need to make sure to reset `allows_reentry` even on failure.
1089
+ let result = try_call ( ) ;
1090
+
1091
+ // Protection is on a per call basis.
1092
+ self . top_frame_mut ( ) . allows_reentry = true ;
1093
+
1094
+ result
1067
1095
}
1068
1096
1069
1097
fn instantiate (
@@ -1097,7 +1125,7 @@ where
1097
1125
beneficiary : & AccountIdOf < Self :: T > ,
1098
1126
) -> Result < u32 , ( DispatchError , u32 ) > {
1099
1127
if self . is_recursive ( ) {
1100
- return Err ( ( Error :: < T > :: ReentranceDenied . into ( ) , 0 ) ) ;
1128
+ return Err ( ( Error :: < T > :: TerminatedWhileReentrant . into ( ) , 0 ) ) ;
1101
1129
}
1102
1130
let frame = self . top_frame_mut ( ) ;
1103
1131
let info = frame. terminate ( ) ;
@@ -1125,7 +1153,7 @@ where
1125
1153
delta : Vec < StorageKey > ,
1126
1154
) -> Result < ( u32 , u32 ) , ( DispatchError , u32 , u32 ) > {
1127
1155
if self . is_recursive ( ) {
1128
- return Err ( ( Error :: < T > :: ReentranceDenied . into ( ) , 0 , 0 ) ) ;
1156
+ return Err ( ( Error :: < T > :: TerminatedWhileReentrant . into ( ) , 0 , 0 ) ) ;
1129
1157
}
1130
1158
let origin_contract = self . top_frame_mut ( ) . contract_info ( ) . clone ( ) ;
1131
1159
let result = Rent :: < T , E > :: restore_to (
@@ -1308,12 +1336,14 @@ mod tests {
1308
1336
exec:: ExportedFunction :: * ,
1309
1337
Error , Weight ,
1310
1338
} ;
1339
+ use codec:: { Encode , Decode } ;
1311
1340
use sp_core:: Bytes ;
1312
1341
use sp_runtime:: DispatchError ;
1313
1342
use assert_matches:: assert_matches;
1314
1343
use std:: { cell:: RefCell , collections:: HashMap , rc:: Rc } ;
1315
1344
use pretty_assertions:: { assert_eq, assert_ne} ;
1316
1345
use pallet_contracts_primitives:: ReturnFlags ;
1346
+ use frame_support:: { assert_ok, assert_err} ;
1317
1347
1318
1348
type MockStack < ' a > = Stack < ' a , Test , MockExecutable > ;
1319
1349
@@ -1731,7 +1761,7 @@ mod tests {
1731
1761
let value = Default :: default ( ) ;
1732
1762
let recurse_ch = MockLoader :: insert ( Call , |ctx, _| {
1733
1763
// Try to call into yourself.
1734
- let r = ctx. ext . call ( 0 , BOB , 0 , vec ! [ ] ) ;
1764
+ let r = ctx. ext . call ( 0 , BOB , 0 , vec ! [ ] , true ) ;
1735
1765
1736
1766
REACHED_BOTTOM . with ( |reached_bottom| {
1737
1767
let mut reached_bottom = reached_bottom. borrow_mut ( ) ;
@@ -1789,7 +1819,7 @@ mod tests {
1789
1819
1790
1820
// Call into CHARLIE contract.
1791
1821
assert_matches ! (
1792
- ctx. ext. call( 0 , CHARLIE , 0 , vec![ ] ) ,
1822
+ ctx. ext. call( 0 , CHARLIE , 0 , vec![ ] , true ) ,
1793
1823
Ok ( _)
1794
1824
) ;
1795
1825
exec_success ( )
@@ -1832,7 +1862,7 @@ mod tests {
1832
1862
1833
1863
// Call into charlie contract.
1834
1864
assert_matches ! (
1835
- ctx. ext. call( 0 , CHARLIE , 0 , vec![ ] ) ,
1865
+ ctx. ext. call( 0 , CHARLIE , 0 , vec![ ] , true ) ,
1836
1866
Ok ( _)
1837
1867
) ;
1838
1868
exec_success ( )
@@ -2263,7 +2293,7 @@ mod tests {
2263
2293
assert_ne ! ( original_allowance, changed_allowance) ;
2264
2294
ctx. ext . set_rent_allowance ( changed_allowance) ;
2265
2295
assert_eq ! (
2266
- ctx. ext. call( 0 , CHARLIE , 0 , vec![ ] ) . map( |v| v. 0 ) . map_err( |e| e. 0 ) ,
2296
+ ctx. ext. call( 0 , CHARLIE , 0 , vec![ ] , true ) . map( |v| v. 0 ) . map_err( |e| e. 0 ) ,
2267
2297
exec_trapped( )
2268
2298
) ;
2269
2299
assert_eq ! ( ctx. ext. rent_allowance( ) , changed_allowance) ;
@@ -2272,7 +2302,7 @@ mod tests {
2272
2302
exec_success ( )
2273
2303
} ) ;
2274
2304
let code_charlie = MockLoader :: insert ( Call , |ctx, _| {
2275
- assert ! ( ctx. ext. call( 0 , BOB , 0 , vec![ 99 ] ) . is_ok( ) ) ;
2305
+ assert ! ( ctx. ext. call( 0 , BOB , 0 , vec![ 99 ] , true ) . is_ok( ) ) ;
2276
2306
exec_trapped ( )
2277
2307
} ) ;
2278
2308
@@ -2299,7 +2329,7 @@ mod tests {
2299
2329
fn recursive_call_during_constructor_fails ( ) {
2300
2330
let code = MockLoader :: insert ( Constructor , |ctx, _| {
2301
2331
assert_matches ! (
2302
- ctx. ext. call( 0 , ctx. ext. address( ) . clone( ) , 0 , vec![ ] ) ,
2332
+ ctx. ext. call( 0 , ctx. ext. address( ) . clone( ) , 0 , vec![ ] , true ) ,
2303
2333
Err ( ( ExecError { error, ..} , _) ) if error == <Error <Test >>:: ContractNotFound . into( )
2304
2334
) ;
2305
2335
exec_success ( )
@@ -2390,4 +2420,84 @@ mod tests {
2390
2420
2391
2421
assert_eq ! ( & String :: from_utf8( debug_buffer) . unwrap( ) , "This is a testMore text" ) ;
2392
2422
}
2423
+
2424
+ #[ test]
2425
+ fn call_reentry_direct_recursion ( ) {
2426
+ // call the contract passed as input with disabled reentry
2427
+ let code_bob = MockLoader :: insert ( Call , |ctx, _| {
2428
+ let dest = Decode :: decode ( & mut ctx. input_data . as_ref ( ) ) . unwrap ( ) ;
2429
+ ctx. ext . call ( 0 , dest, 0 , vec ! [ ] , false ) . map ( |v| v. 0 ) . map_err ( |e| e. 0 )
2430
+ } ) ;
2431
+
2432
+ let code_charlie = MockLoader :: insert ( Call , |_, _| {
2433
+ exec_success ( )
2434
+ } ) ;
2435
+
2436
+ ExtBuilder :: default ( ) . build ( ) . execute_with ( || {
2437
+ let schedule = <Test as Config >:: Schedule :: get ( ) ;
2438
+ place_contract ( & BOB , code_bob) ;
2439
+ place_contract ( & CHARLIE , code_charlie) ;
2440
+
2441
+ // Calling another contract should succeed
2442
+ assert_ok ! ( MockStack :: run_call(
2443
+ ALICE ,
2444
+ BOB ,
2445
+ & mut GasMeter :: <Test >:: new( GAS_LIMIT ) ,
2446
+ & schedule,
2447
+ 0 ,
2448
+ CHARLIE . encode( ) ,
2449
+ None ,
2450
+ ) ) ;
2451
+
2452
+ // Calling into oneself fails
2453
+ assert_err ! (
2454
+ MockStack :: run_call(
2455
+ ALICE ,
2456
+ BOB ,
2457
+ & mut GasMeter :: <Test >:: new( GAS_LIMIT ) ,
2458
+ & schedule,
2459
+ 0 ,
2460
+ BOB . encode( ) ,
2461
+ None ,
2462
+ ) . map_err( |e| e. 0 . error) ,
2463
+ <Error <Test >>:: ReentranceDenied ,
2464
+ ) ;
2465
+ } ) ;
2466
+ }
2467
+
2468
+ #[ test]
2469
+ fn call_deny_reentry ( ) {
2470
+ let code_bob = MockLoader :: insert ( Call , |ctx, _| {
2471
+ if ctx. input_data [ 0 ] == 0 {
2472
+ ctx. ext . call ( 0 , CHARLIE , 0 , vec ! [ ] , false ) . map ( |v| v. 0 ) . map_err ( |e| e. 0 )
2473
+ } else {
2474
+ exec_success ( )
2475
+ }
2476
+ } ) ;
2477
+
2478
+ // call BOB with input set to '1'
2479
+ let code_charlie = MockLoader :: insert ( Call , |ctx, _| {
2480
+ ctx. ext . call ( 0 , BOB , 0 , vec ! [ 1 ] , true ) . map ( |v| v. 0 ) . map_err ( |e| e. 0 )
2481
+ } ) ;
2482
+
2483
+ ExtBuilder :: default ( ) . build ( ) . execute_with ( || {
2484
+ let schedule = <Test as Config >:: Schedule :: get ( ) ;
2485
+ place_contract ( & BOB , code_bob) ;
2486
+ place_contract ( & CHARLIE , code_charlie) ;
2487
+
2488
+ // BOB -> CHARLIE -> BOB fails as BOB denies reentry.
2489
+ assert_err ! (
2490
+ MockStack :: run_call(
2491
+ ALICE ,
2492
+ BOB ,
2493
+ & mut GasMeter :: <Test >:: new( GAS_LIMIT ) ,
2494
+ & schedule,
2495
+ 0 ,
2496
+ vec![ 0 ] ,
2497
+ None ,
2498
+ ) . map_err( |e| e. 0 . error) ,
2499
+ <Error <Test >>:: ReentranceDenied ,
2500
+ ) ;
2501
+ } ) ;
2502
+ }
2393
2503
}
0 commit comments