Skip to content
Open
13 changes: 13 additions & 0 deletions contracts/async-vault/src/extend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! OpenZeppelin extends a role entry when it reads it, not when it creates it.

use soroban_sdk::{Address, Env, Symbol};
use stellar_access::access_control;

/// The role, its member-list slot, the member count and the role list: all a grant writes.
pub(crate) fn role_entries(e: &Env, account: &Address, role: &Symbol) {
if let Some(index) = access_control::has_role(e, account, role) {
access_control::get_role_member(e, role, index);
}
access_control::get_role_member_count(e, role);
access_control::get_existing_roles(e);
}
4 changes: 3 additions & 1 deletion contracts/async-vault/src/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use strata_timelock::{queue, setup, Clock, Refusal};

use crate::error::VaultError;
use crate::event::{ChangeApplied, ChangeCancelled, ChangeProposed, NoticeSet, SetupFinished};
use crate::extend;
use crate::guards;
use crate::keys::DataKey;
use crate::state;
Expand Down Expand Up @@ -247,7 +248,8 @@ pub(crate) fn apply(e: &Env, id: u32) {
VaultChange::Notice(secs) => set_notice(e, *secs),
VaultChange::WindDownDelay(secs) => wind_down::set_delay(e, *secs),
VaultChange::GrantRole(role, account) => {
access_control::grant_role_no_auth(e, account, role, &admin)
access_control::grant_role_no_auth(e, account, role, &admin);
extend::role_entries(e, account, role);
}
VaultChange::RevokeRole(role, account) => {
access_control::revoke_role_no_auth(e, account, role, &admin)
Expand Down
5 changes: 5 additions & 0 deletions contracts/async-vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod deposit;
mod epoch;
mod error;
mod event;
mod extend;
mod governance;
mod guards;
mod health;
Expand Down Expand Up @@ -86,8 +87,11 @@ impl AsyncVault {

access_control::set_admin(e, &roles.governance);
access_control::grant_role_no_auth(e, &roles.manager, &MANAGER_ROLE, &roles.governance);
extend::role_entries(e, &roles.manager, &MANAGER_ROLE);
access_control::grant_role_no_auth(e, &roles.treasury, &TREASURY_ROLE, &roles.governance);
extend::role_entries(e, &roles.treasury, &TREASURY_ROLE);
access_control::grant_role_no_auth(e, &roles.guardian, &GUARDIAN_ROLE, &roles.governance);
extend::role_entries(e, &roles.guardian, &GUARDIAN_ROLE);

state::set_addr(e, &DataKey::Asset, &asset);
state::set_addr(e, &DataKey::ShareToken, &share_token);
Expand Down Expand Up @@ -404,6 +408,7 @@ impl AccessControl for AsyncVault {
fn grant_role(e: &Env, account: Address, role: Symbol, caller: Address) {
guards::direct_change_allowed(e);
access_control::grant_role(e, &account, &role, &caller);
extend::role_entries(e, &account, &role);
}

/// Instant for every role but `manager`: taking `treasury` or `guardian`
Expand Down
140 changes: 140 additions & 0 deletions contracts/async-vault/src/test/ttl.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,76 @@
use soroban_sdk::testutils::storage::{Instance as _, Persistent as _};
use soroban_sdk::{contracttype, IntoVal, Symbol, Val};
use stellar_access::access_control::{AccessControlStorageKey, ROLE_EXTEND_AMOUNT};
use stellar_tokens::fungible::{FungibleStorageKey, BALANCE_EXTEND_AMOUNT};
use strata_storage::{INSTANCE_EXTEND_AMOUNT, PERSISTENT_EXTEND_AMOUNT};

use super::*;
use crate::keys::DataKey;
use crate::roles::{GUARDIAN_ROLE, MANAGER_ROLE, TREASURY_ROLE};

const TWO_DAYS: u64 = 2 * DAY_DURATION;

/// Testnet's minimum lifetime of a new persistent entry, in ledgers.
const TESTNET_MIN_PERSISTENT_TTL: u32 = 120_960;

fn persistent_ttl(f: &Fixture, key: &DataKey) -> u32 {
f.e.as_contract(&f.vault.address, || f.e.storage().persistent().get_ttl(key))
}

fn ttl_in<K: IntoVal<Env, Val>>(e: &Env, contract: &Address, key: &K) -> u32 {
e.as_contract(contract, || e.storage().persistent().get_ttl(key))
}

/// Mirrors OpenZeppelin's private member-list key: same variant and fields, same encoding.
#[contracttype]
struct RoleAccountKey {
role: Symbol,
index: u32,
}

#[contracttype]
enum RoleMemberKey {
RoleAccounts(RoleAccountKey),
}

/// Every entry a grant writes: the role, its member-list slot, the member count, the role list.
fn role_entry_ttls(e: &Env, contract: &Address, account: &Address, role: &Symbol) -> [u32; 4] {
let has_role = AccessControlStorageKey::HasRole(account.clone(), role.clone());
let index: u32 = e.as_contract(contract, || {
e.storage().persistent().get(&has_role).unwrap()
});
[
ttl_in(e, contract, &has_role),
ttl_in(
e,
contract,
&RoleMemberKey::RoleAccounts(RoleAccountKey {
role: role.clone(),
index,
}),
),
ttl_in(
e,
contract,
&AccessControlStorageKey::RoleAccountsCount(role.clone()),
),
ttl_in(e, contract, &AccessControlStorageKey::ExistingRoles),
]
}

fn share_balance_ttl(f: &Fixture, account: &Address) -> u32 {
ttl_in(
&f.e,
&f.share.address,
&FungibleStorageKey::Balance(account.clone()),
)
}

fn testnet_minimum(e: &Env) {
e.ledger()
.set_min_persistent_entry_ttl(TESTNET_MIN_PERSISTENT_TTL);
}

fn instance_ttl(f: &Fixture) -> u32 {
f.e.as_contract(&f.vault.address, || f.e.storage().instance().get_ttl())
}
Expand Down Expand Up @@ -106,3 +167,82 @@ fn a_pending_request_still_claims_after_most_of_a_month() {

assert_eq!(f.vault.claim_deposit(&user, &epoch), 200);
}

#[test]
fn a_deposit_claim_writes_the_new_share_balance_with_the_full_ttl() {
let f = setup();
let user = f.investor(1_000);
let epoch = f.vault.request_deposit(&user, &400);
f.fulfill_epoch(wad(2));
testnet_minimum(&f.e);

f.vault.claim_deposit(&user, &epoch);

assert_eq!(share_balance_ttl(&f, &user), BALANCE_EXTEND_AMOUNT);
}

#[test]
fn the_first_redemption_writes_the_vaults_share_balance_with_the_full_ttl() {
let f = setup();
let holder = f.holder(100);
testnet_minimum(&f.e);

f.vault.request_redeem(&holder, &100);

assert_eq!(
share_balance_ttl(&f, &f.vault.address),
BALANCE_EXTEND_AMOUNT
);
}

#[test]
fn the_constructor_writes_each_role_with_the_full_ttl() {
let e = Env::default();
testnet_minimum(&e);
let roles = distinct_roles(&e);

let vault = register_with(&e, roles.clone());

for (account, role) in [
(&roles.manager, MANAGER_ROLE),
(&roles.treasury, TREASURY_ROLE),
(&roles.guardian, GUARDIAN_ROLE),
] {
assert_eq!(
role_entry_ttls(&e, &vault, account, &role),
[ROLE_EXTEND_AMOUNT; 4]
);
}
}

#[test]
fn a_grant_during_setup_writes_the_role_with_the_full_ttl() {
let f = setup_unfinished();
let next = Address::generate(&f.e);
testnet_minimum(&f.e);

f.vault.grant_role(&next, &GUARDIAN_ROLE, &f.admin);

assert_eq!(
role_entry_ttls(&f.e, &f.vault.address, &next, &GUARDIAN_ROLE),
[ROLE_EXTEND_AMOUNT; 4]
);
}

#[test]
fn a_grant_through_the_timelock_writes_the_role_with_the_full_ttl() {
let f = setup();
let next = Address::generate(&f.e);
let id = f
.vault
.propose_change(&VaultChange::GrantRole(GUARDIAN_ROLE, next.clone()));
f.advance(MIN_UPGRADE_DELAY);
testnet_minimum(&f.e);

f.vault.apply_change(&id);

assert_eq!(
role_entry_ttls(&f.e, &f.vault.address, &next, &GUARDIAN_ROLE),
[ROLE_EXTEND_AMOUNT; 4]
);
}
13 changes: 13 additions & 0 deletions contracts/nav-oracle/src/extend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! OpenZeppelin extends a role entry when it reads it, not when it creates it.

use soroban_sdk::{Address, Env, Symbol};
use stellar_access::access_control;

/// The role, its member-list slot, the member count and the role list: all a grant writes.
pub(crate) fn role_entries(e: &Env, account: &Address, role: &Symbol) {
if let Some(index) = access_control::has_role(e, account, role) {
access_control::get_role_member(e, role, index);
}
access_control::get_role_member_count(e, role);
access_control::get_existing_roles(e);
}
4 changes: 3 additions & 1 deletion contracts/nav-oracle/src/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use stellar_access::{
use stellar_contract_utils::upgradeable;
use strata_timelock::{queue, setup, Clock, Refusal, TimelockHost};

use crate::extend;
use crate::guards;
use crate::state::OracleConfig;
use crate::{
Expand Down Expand Up @@ -121,7 +122,8 @@ pub(crate) fn apply(e: &Env, id: u32) {
match &change {
OracleChange::Upgrade(hash) => upgradeable::upgrade(e, hash),
OracleChange::GrantRole(role, account) => {
access_control::grant_role_no_auth(e, account, role, &admin)
access_control::grant_role_no_auth(e, account, role, &admin);
extend::role_entries(e, account, role);
}
OracleChange::RevokeRole(role, account) => {
access_control::revoke_role_no_auth(e, account, role, &admin)
Expand Down
4 changes: 4 additions & 0 deletions contracts/nav-oracle/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![no_std]

mod extend;
mod governance;
mod guards;
mod state;
Expand Down Expand Up @@ -189,6 +190,7 @@ impl AccessControl for NavOracleContract {
guards::direct_change_allowed(e);
strata_storage::bump_instance(e);
access_control::grant_role(e, &account, &role, &caller);
extend::role_entries(e, &account, &role);
}

/// Cancelling a pending hand-over (`live_until_ledger == 0`) keeps the
Expand Down Expand Up @@ -236,7 +238,9 @@ impl NavOracleContract {
guards::valid_config(e, &config);
access_control::set_admin(e, &admin);
access_control::grant_role_no_auth(e, &attester, &ATTESTER_ROLE, &admin);
extend::role_entries(e, &attester, &ATTESTER_ROLE);
access_control::grant_role_no_auth(e, &guardian, &GUARDIAN_ROLE, &admin);
extend::role_entries(e, &guardian, &GUARDIAN_ROLE);

state::set_config(e, &config);
state::set_nav_paused(e, false);
Expand Down
Loading
Loading