Skip to content

Commit ec1e5cf

Browse files
committed
feat(access-manager): simple reentrancy test
1 parent 8e24cf8 commit ec1e5cf

File tree

6 files changed

+296
-65
lines changed

6 files changed

+296
-65
lines changed

cosmwasm/access-manager/src/contract.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use crate::{
3636
/// ```
3737
///
3838
/// <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/access/manager/AccessManager.sol#L123>
39+
#[inline]
3940
fn only_authorized(ctx: &mut ExecCtx) -> Result<(), ContractError> {
4041
_check_authorized(ctx)
4142
}
@@ -144,7 +145,7 @@ pub(crate) fn set_grant_delay(
144145
/// ) internal virtual returns (bool)
145146
/// ```
146147
///
147-
/// <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/access/manager/AccessManager.sol#L270-L275>
148+
/// <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/access/manager/AccessManager.sol#L270>
148149
pub(crate) fn _grant_role(
149150
ctx: &mut ExecCtx,
150151
role_id: RoleId,
@@ -446,7 +447,7 @@ pub(crate) fn set_target_closed(
446447
/// function _setTargetClosed(address target, bool closed) internal virtual
447448
/// ```
448449
///
449-
/// <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/access/manager/AccessManager.sol#L422C5-L422C76>
450+
/// <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/access/manager/AccessManager.sol#L422>
450451
fn _set_target_closed(ctx: &mut ExecCtx, target: &Addr, closed: bool) -> Result<(), ContractError> {
451452
ctx.storage()
452453
.upsert::<Targets, ContractError>(target, |maybe_target_config| {
@@ -496,11 +497,13 @@ pub(crate) fn schedule(
496497
) -> Result<(H256, u32), ContractError> {
497498
let caller = ctx.msg_sender();
498499

500+
dbg!(data);
501+
499502
// Fetch restrictions that apply to the caller on the targeted function
500503
let CanCall {
501504
allowed: _,
502505
delay: setback,
503-
} = _can_call_extended(ctx, caller, target, data)?;
506+
} = dbg!(_can_call_extended(ctx, caller, target, data)?);
504507

505508
let min_when = ctx.timestamp() + u64::from(setback);
506509

@@ -769,6 +772,7 @@ pub(crate) fn update_authority(
769772
/// ```
770773
///
771774
/// <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/access/manager/AccessManager.sol#L598>
775+
#[inline]
772776
fn _check_authorized(ctx: &mut ExecCtx) -> Result<(), ContractError> {
773777
let msg_data = ctx.msg_data();
774778

@@ -811,7 +815,7 @@ fn _check_authorized(ctx: &mut ExecCtx) -> Result<(), ContractError> {
811815
/// ) private view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay)
812816
/// ```
813817
///
814-
/// <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/access/manager/AccessManager.sol#L619-L621>
818+
/// <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/access/manager/AccessManager.sol#L619>
815819
fn _get_admin_restrictions(
816820
ctx: &mut ExecCtx,
817821
data: &str,
@@ -840,9 +844,11 @@ fn _get_admin_restrictions(
840844

841845
// Restricted to that role's admin with no delay beside any execution delay the caller may
842846
// have.
843-
Ok(GrantRole { role_id, .. } | RevokeRole { role_id, .. }) => {
844-
Ok((true, get_role_admin(ctx.query_ctx(), role_id)?, 0))
845-
}
847+
Ok(GrantRole { role_id, .. } | RevokeRole { role_id, .. }) => Ok((
848+
true,
849+
dbg!(get_role_admin(ctx.query_ctx(), dbg!(role_id))?),
850+
0,
851+
)),
846852

847853
_ => Ok((
848854
false,
@@ -890,7 +896,7 @@ fn _can_call_extended(
890896
data: &str,
891897
) -> Result<CanCall, ContractError> {
892898
if target == ctx.address_this() {
893-
_can_call_self(ctx, caller, data)
899+
dbg!(_can_call_self(ctx, caller, data))
894900
} else {
895901
can_call(ctx.query_ctx(), caller, target, _check_selector(data)?)
896902
}
@@ -905,7 +911,7 @@ fn _can_call_extended(
905911
/// <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/access/manager/AccessManager.sol#L685>
906912
fn _can_call_self(ctx: &mut ExecCtx, caller: &Addr, data: &str) -> Result<CanCall, ContractError> {
907913
if caller == ctx.address_this() {
908-
// Caller is AccessManager, this means the call was sent through {execute} and it already
914+
// Caller is AccessManager, this means the call was sent through `execute` and it already
909915
// checked permissions. We verify that the call "identifier", which is set during
910916
// `execute`, is correct.
911917
return Ok(CanCall {
@@ -916,6 +922,8 @@ fn _can_call_self(ctx: &mut ExecCtx, caller: &Addr, data: &str) -> Result<CanCal
916922

917923
let (admin_restricted, role_id, operation_delay) = _get_admin_restrictions(ctx, data)?;
918924

925+
dbg!((admin_restricted, role_id, operation_delay));
926+
919927
// isTargetClosed apply to non-admin-restricted function
920928
if !admin_restricted && is_target_closed(ctx.query_ctx(), ctx.address_this())? {
921929
return Ok(CanCall {
@@ -927,7 +935,7 @@ fn _can_call_self(ctx: &mut ExecCtx, caller: &Addr, data: &str) -> Result<CanCal
927935
let HasRole {
928936
is_member,
929937
execution_delay,
930-
} = has_role(ctx.query_ctx(), role_id, caller)?;
938+
} = dbg!(has_role(ctx.query_ctx(), role_id, caller)?);
931939

932940
if !is_member {
933941
return Ok(CanCall {

cosmwasm/access-manager/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
//!
1616
//! For each target contract, admins can configure the following without any delay:
1717
//!
18-
//! - The target's {AccessManaged-authority} via [`ExecuteMsg::UpdateAuthority`].
18+
//! - The target's [`QueryMsg::Authority`][access_manager_types::managed::msg::QueryMsg::Authority]
19+
//! via [`ExecuteMsg::UpdateAuthority`].
1920
//! - Close or open a target via [`ExecuteMsg::SetTargetClosed`] keeping the permissions intact.
2021
//! - The roles that are allowed (or disallowed) to call a given function (identified by its
2122
//! selector) through [`ExecuteMsg::SetTargetAdminDelay`].

cosmwasm/access-manager/src/tests.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,3 +1408,100 @@ fn schedule_works() {
14081408
}),
14091409
);
14101410
}
1411+
1412+
#[test]
1413+
fn schedule_reentrant_works() {
1414+
const GRANT_ROLE: RoleId = RoleId::new(6);
1415+
1416+
let (mut deps, env) = setup();
1417+
1418+
execute(
1419+
deps.as_mut(),
1420+
env.clone(),
1421+
message_info(&ADMIN, &[]),
1422+
ExecuteMsg::SetTargetFunctionRole {
1423+
target: env.contract.address.clone(),
1424+
selectors: vec![Selector::new("grant_role").to_owned()],
1425+
role_id: GRANT_ROLE,
1426+
},
1427+
)
1428+
.unwrap();
1429+
1430+
execute(
1431+
deps.as_mut(),
1432+
env.clone(),
1433+
message_info(&ADMIN, &[]),
1434+
ExecuteMsg::GrantRole {
1435+
account: ACCOUNT_1.clone(),
1436+
role_id: GRANT_ROLE,
1437+
execution_delay: 10,
1438+
},
1439+
)
1440+
.unwrap();
1441+
1442+
execute(
1443+
deps.as_mut(),
1444+
env.clone(),
1445+
message_info(&ADMIN, &[]),
1446+
ExecuteMsg::GrantRole {
1447+
account: ACCOUNT_1.clone(),
1448+
role_id: RoleId::new(11),
1449+
execution_delay: 20,
1450+
},
1451+
)
1452+
.unwrap();
1453+
1454+
execute(
1455+
deps.as_mut(),
1456+
env.clone(),
1457+
message_info(&ADMIN, &[]),
1458+
ExecuteMsg::SetRoleAdmin {
1459+
role_id: RoleId::new(10),
1460+
admin: RoleId::new(11),
1461+
},
1462+
)
1463+
.unwrap();
1464+
1465+
execute(
1466+
deps.as_mut(),
1467+
env.clone(),
1468+
message_info(&ADMIN, &[]),
1469+
ExecuteMsg::SetTargetFunctionRole {
1470+
role_id: GRANT_ROLE,
1471+
target: env.contract.address.clone(),
1472+
selectors: vec![Selector::new("grant_role").to_owned()],
1473+
},
1474+
)
1475+
.unwrap();
1476+
1477+
eprintln!("NOW\n\n\n");
1478+
1479+
assert_eq!(
1480+
execute(
1481+
deps.as_mut(),
1482+
env.clone(),
1483+
message_info(&ACCOUNT_1, &[]),
1484+
ExecuteMsg::Schedule {
1485+
target: env.contract.address.clone(),
1486+
data: serde_json::to_string(&ExecuteMsg::GrantRole {
1487+
role_id: RoleId::new(10),
1488+
account: ACCOUNT_2.clone(),
1489+
execution_delay: 0
1490+
})
1491+
.unwrap(),
1492+
when: env.block.time.seconds() + 20
1493+
},
1494+
)
1495+
.unwrap(),
1496+
Response::new().add_event(OperationScheduled {
1497+
operation_id: H256::new(hex!(
1498+
"8851fd1669d010b077f22bf956ea2ae240fe964d0f9d30e46f702b6e950278b5"
1499+
)),
1500+
nonce: 1,
1501+
schedule: env.block.time.seconds() + 20,
1502+
caller: &ACCOUNT_1,
1503+
target: &env.contract.address,
1504+
data: r#"{"grant_role":{"role_id":"10","account":"account-2","execution_delay":0}}"#,
1505+
}),
1506+
);
1507+
}

e2e/access-manager-tests/src/main.rs

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ const DECREMENT: RoleId = RoleId::new(2);
5656
const INCREMENT_GUARDIAN: RoleId = RoleId::new(4);
5757
const DECREMENT_GUARDIAN: RoleId = RoleId::new(5);
5858

59+
const GRANT_ROLE: RoleId = RoleId::new(6);
60+
const GRANT_ROLE_ADMIN: RoleId = RoleId::new(7);
61+
5962
fn mk_wallet(mnemonic: &str, bech32_prefix: &str) -> LocalSigner {
6063
LocalSigner::new(
6164
tiny_hderive::bip32::ExtendedPrivKey::derive(
@@ -220,11 +223,13 @@ async fn main() -> Result<()> {
220223
)
221224
.await?;
222225

223-
schedule_increment(&alice_client, &bob_client, &manager, &managed).await?;
226+
// schedule_increment(&alice_client, &bob_client, &manager, &managed).await?;
227+
228+
// schedule_decrement_in_sub_msg(&alice_client, &bob_client, &manager, &managed).await?;
224229

225-
schedule_decrement_in_sub_msg(&alice_client, &bob_client, &manager, &managed).await?;
230+
// schedule_increment_in_reply(&alice_client, &bob_client, &manager, &managed).await?;
226231

227-
schedule_increment_in_reply(&alice_client, &bob_client, &manager, &managed).await?;
232+
schedule_reentrant(&alice_client, &bob_client, &charlie_client, &manager).await?;
228233

229234
Ok(())
230235
}
@@ -544,6 +549,144 @@ async fn schedule_increment_in_reply(
544549
Ok(())
545550
}
546551

552+
#[instrument(skip_all)]
553+
async fn schedule_reentrant(
554+
alice_client: &TxClient<impl WalletT, impl RpcT, impl GasFillerT>,
555+
bob_client: &TxClient<impl WalletT, impl RpcT, impl GasFillerT>,
556+
charlie_client: &TxClient<impl WalletT, impl RpcT, impl GasFillerT>,
557+
manager: &Bech32<H256>,
558+
) -> Result<()> {
559+
execute(
560+
alice_client,
561+
manager,
562+
[
563+
&manager::msg::ExecuteMsg::SetTargetFunctionRole {
564+
target: Addr::unchecked(manager.to_string()),
565+
selectors: vec![Selector::new("grant_role").to_owned()],
566+
role_id: GRANT_ROLE,
567+
},
568+
&manager::msg::ExecuteMsg::GrantRole {
569+
account: Addr::unchecked(charlie_client.wallet().address().to_string()),
570+
role_id: GRANT_ROLE,
571+
execution_delay: 10,
572+
},
573+
&manager::msg::ExecuteMsg::GrantRole {
574+
account: Addr::unchecked(charlie_client.wallet().address().to_string()),
575+
role_id: RoleId::new(11),
576+
execution_delay: 20,
577+
},
578+
&manager::msg::ExecuteMsg::SetRoleAdmin {
579+
role_id: RoleId::new(10),
580+
admin: RoleId::new(11),
581+
},
582+
&manager::msg::ExecuteMsg::SetTargetFunctionRole {
583+
role_id: GRANT_ROLE,
584+
target: Addr::unchecked(manager.to_string()),
585+
selectors: vec![Selector::new("grant_role").to_owned()],
586+
},
587+
],
588+
)
589+
.await?;
590+
591+
// let msg = manager::msg::ExecuteMsg::Schedule {
592+
// target: Addr::unchecked(manager.to_string()),
593+
// data: serde_json::to_string(&manager::msg::ExecuteMsg::GrantRole {
594+
// account: Addr::unchecked(bob_client.wallet().address().to_string()),
595+
// role_id: RoleId::new(7),
596+
// execution_delay: 10,
597+
// })
598+
// .unwrap(),
599+
// when: now() + 10,
600+
// };
601+
let msg = manager::msg::ExecuteMsg::GrantRole {
602+
role_id: RoleId::new(10),
603+
account: Addr::unchecked(bob_client.wallet().address().to_string()),
604+
execution_delay: 0,
605+
};
606+
let data = to_json_string(&msg).unwrap();
607+
let operation_id = hash_operation(charlie_client.wallet().address(), manager, &msg);
608+
609+
info!(
610+
"charlie can't call grant_role since they have an execution delay, they must schedule the call"
611+
);
612+
execute_expect_error(
613+
charlie_client,
614+
manager,
615+
&msg,
616+
AccessManagerError::AccessManagerNotScheduled(operation_id),
617+
)
618+
.await?;
619+
620+
info!("schedule grant_role call too soon");
621+
execute_expect_error(
622+
charlie_client,
623+
manager,
624+
&manager::msg::ExecuteMsg::Schedule {
625+
target: Addr::unchecked(manager.to_string()),
626+
data: data.clone(),
627+
when: now() + 5,
628+
},
629+
AccessManagerError::AccessManagerUnauthorizedCall {
630+
caller: Addr::unchecked(charlie_client.wallet().address().to_string()),
631+
target: Addr::unchecked(manager.to_string()),
632+
selector: Selector::new("grant_role").to_owned(),
633+
},
634+
)
635+
.await?;
636+
637+
info!("schedule grant_role call with correct delay");
638+
let when = now() + 25;
639+
execute(
640+
charlie_client,
641+
manager,
642+
[manager::msg::ExecuteMsg::Schedule {
643+
target: Addr::unchecked(manager.to_string()),
644+
data: data.clone(),
645+
when,
646+
}],
647+
)
648+
.await?;
649+
650+
info!("execute scheduled call too soon");
651+
execute_expect_error(
652+
charlie_client,
653+
manager,
654+
&manager::msg::ExecuteMsg::Execute {
655+
target: Addr::unchecked(manager.to_string()),
656+
data: data.clone(),
657+
},
658+
AccessManagerError::AccessManagerNotReady(operation_id),
659+
)
660+
.await?;
661+
662+
info!("scheduled op not ready");
663+
execute_expect_error(
664+
charlie_client,
665+
manager,
666+
&msg,
667+
AccessManagerError::AccessManagerNotReady(operation_id),
668+
)
669+
.await?;
670+
671+
wait_for_finalized_block_with(alice_client.rpc().client(), |block| {
672+
block.block.header.time.seconds.inner() as u64 > when
673+
})
674+
.await?;
675+
676+
info!("execute scheduled call once ready");
677+
execute(
678+
charlie_client,
679+
manager,
680+
[manager::msg::ExecuteMsg::Execute {
681+
target: Addr::unchecked(manager.to_string()),
682+
data: data.clone(),
683+
}],
684+
)
685+
.await?;
686+
687+
Ok(())
688+
}
689+
547690
fn now() -> u64 {
548691
std::time::SystemTime::now()
549692
.duration_since(std::time::UNIX_EPOCH)
@@ -668,6 +811,7 @@ async fn execute(
668811
Ok(())
669812
}
670813

814+
// #[track_caller]
671815
async fn execute_expect_error(
672816
signer: &TxClient<impl WalletT, impl RpcT, impl GasFillerT>,
673817
contract: &Bech32<H256>,

0 commit comments

Comments
 (0)