Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions contracts/async-vault/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,18 @@ fn setup<'a>() -> Fixture<'a> {
/// Builds the vault against a caller-chosen feed, so cooldown, freshness and the
/// deviation cap can be exercised from a vault test rather than only an oracle one.
fn setup_with_feed<'a>(feed: OracleConfig) -> Fixture<'a> {
setup_with(feed, |e, args| e.register(ShareToken, args))
}

/// The share token's constructor arguments, in order.
type TokenArgs = (String, String, Address, Address, Address, Address);

/// Builds the vault around a token the caller registers, so a test can run the
/// token's built Wasm instead of its native code.
fn setup_with<'a>(
feed: OracleConfig,
register_token: impl FnOnce(&Env, TokenArgs) -> Address,
) -> Fixture<'a> {
let e = Env::default();
e.mock_all_auths();
e.ledger().set_timestamp(10_000);
Expand All @@ -221,8 +233,8 @@ fn setup_with_feed<'a>(feed: OracleConfig) -> Fixture<'a> {

let share = ShareTokenClient::new(
&e,
&e.register(
ShareToken,
&register_token(
&e,
(
String::from_str(&e, "Strata Vault USDC"),
String::from_str(&e, "bvUSDC"),
Expand Down
57 changes: 57 additions & 0 deletions contracts/async-vault/src/test/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,12 +411,69 @@ fn the_vault_reports_its_clock_to_the_token_and_the_oracle() {

#[cfg(feature = "upgrade_wasm")]
mod with_wasm {
use share_token::TokenChange;

use super::*;

soroban_sdk::contractimport!(
file = "../../fixtures/upgrade-target/target/wasm32v1-none/release/upgrade_target.wasm"
);

mod token_v1 {
soroban_sdk::contractimport!(file = "../../target/wasm32v1-none/release/share_token.wasm");
}

mod token_probe {
soroban_sdk::contractimport!(
file = "../../target/upgrade-probe/wasm32v1-none/release/share_token.wasm"
);
}

#[test]
fn the_token_upgrades_under_live_balances_and_open_requests() {
let f = setup_with(open_feed(), |e, args| e.register(token_v1::WASM, args));
let holder = f.holder(100);
let other = f.investor(1);
let epoch = f.vault.request_redeem(&holder, &40);
let depositor = f.investor(500);
assert_eq!(f.vault.request_deposit(&depositor, &500), epoch);

f.share.bind_vault(&f.vault.address, &f.admin);
f.share.finish_setup(&f.admin);
let probe_hash = f.e.deployer().upload_contract_wasm(token_probe::WASM);
let id = f
.share
.propose_change(&TokenChange::Upgrade(probe_hash), &f.admin);
f.advance(MIN_UPGRADE_DELAY);
f.share.apply_change(&id, &f.admin);

let probe = token_probe::Client::new(&f.e, &f.share.address);
assert!(probe.probe());
assert_eq!(probe.schema_version(), 1);
probe.migrate(&f.admin);
assert_eq!(probe.schema_version(), 2);
assert_eq!(
probe.try_migrate(&f.admin),
Err(Ok(token_probe::ProbeError::AlreadyMigrated.into()))
);

assert_eq!(f.shares(&holder), 60);
assert_eq!(f.shares(&f.vault.address), 40);
assert_eq!(
f.vault.get_redeem_request(&epoch, &holder).unwrap().shares,
40
);

f.fulfill_epoch_at(f.close_epoch(), wad(2));
assert_eq!(f.vault.claim_redeem(&holder, &epoch), 80);
assert_eq!(f.vault.claim_deposit(&depositor, &epoch), 250);
assert_eq!(f.shares(&depositor), 250);
assert_eq!(f.shares(&f.vault.address), 0);

f.share.transfer(&holder, &other, &10);
assert_eq!(f.shares(&other), 10);
}

#[test]
fn immediate_upgrade_with_valid_wasm_does_not_replace_code() {
let f = setup();
Expand Down
6 changes: 6 additions & 0 deletions contracts/share-token/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ stellar-macros = { workspace = true }
stellar-tokens = { workspace = true }
stellar-contract-utils = { workspace = true }
storage = { path = "../../crates/storage" }
timelock = { path = "../../crates/timelock" }
bindings = { path = "../../crates/bindings" }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
compliance = { path = "../compliance" }
identity-verifier = { path = "../identity-verifier" }

[features]
# Builds a stand-in for a later token version, used only by the upgrade tests.
upgrade_probe = []
128 changes: 126 additions & 2 deletions contracts/share-token/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use crate::error::ShareTokenError;
use crate::governance::{self, TokenChange, TokenPendingChange};
use crate::roles::COMPLIANCE_ROLE;

use bindings::VaultBinding;
use soroban_sdk::{
contract, contractimpl, panic_with_error, Address, Env, MuxedAddress, String, Symbol, Vec,
};
use stellar_access::access_control::{self as access_control, AccessControl};
use stellar_contract_utils::pausable::{self as pausable, Pausable};
use stellar_contract_utils::upgradeable;
use stellar_macros::{only_admin, only_any_role, only_role};
use stellar_tokens::{
fungible::{Base, ContractOverrides, FungibleToken},
Expand Down Expand Up @@ -36,8 +39,69 @@ impl ShareToken {
);
RWA::set_compliance(e, &compliance);
RWA::set_identity_verifier(e, &identity_verifier);
upgradeable::set_schema_version(e, 1);
storage::bump_instance(e);
}

pub fn schema_version(e: &Env) -> u32 {
upgradeable::get_schema_version(e)
}

/// Names the vault whose clock this token's timelock reads. Once, and only
/// during setup.
#[only_admin]
pub fn bind_vault(e: &Env, vault: Address, caller: Address) {
caller.require_auth();
storage::bump_instance(e);
governance::bind_vault(e, &vault);
}

/// Ends the window in which admin changes are instant. It never reopens.
#[only_admin]
pub fn finish_setup(e: &Env, caller: Address) {
caller.require_auth();
storage::bump_instance(e);
governance::finish_setup(e);
}

/// Queues a change behind the vault's timelock and returns its id.
#[only_admin]
pub fn propose_change(e: &Env, change: TokenChange, caller: Address) -> u32 {
caller.require_auth();
storage::bump_instance(e);
governance::propose(e, change)
}

#[only_admin]
pub fn cancel_change(e: &Env, id: u32, caller: Address) {
caller.require_auth();
storage::bump_instance(e);
governance::cancel(e, id);
}

/// Lands a change whose delay has run. Refused while the vault is paused
/// or winding down.
#[only_admin]
pub fn apply_change(e: &Env, id: u32, caller: Address) {
caller.require_auth();
storage::bump_instance(e);
governance::apply(e, id);
}

pub fn pending_changes(e: &Env) -> Vec<TokenPendingChange> {
governance::pending(e)
}
}

#[contractimpl(contracttrait)]
impl VaultBinding for ShareToken {
fn vault(e: &Env) -> Option<Address> {
timelock::setup::source(e)
}

fn setup_open(e: &Env) -> bool {
timelock::setup::is_open(e)
}
}

#[contractimpl(contracttrait)]
Expand Down Expand Up @@ -86,9 +150,40 @@ impl FungibleToken for ShareToken {

#[contractimpl(contracttrait)]
impl AccessControl for ShareToken {
/// Instant during setup; after it, propose the grant through the timelock.
fn grant_role(e: &Env, account: Address, role: Symbol, caller: Address) {
governance::require_setup_open(e);
storage::bump_instance(e);
access_control::grant_role(e, &account, &role, &caller);
}

/// Instant for `compliance`. Revoking `manager` waits like a grant,
/// because the vault needs it to pay claims.
fn revoke_role(e: &Env, account: Address, role: Symbol, caller: Address) {
governance::require_revoke_allowed(e, &role);
storage::bump_instance(e);
access_control::revoke_role(e, &account, &role, &caller);
}

/// Cancelling a pending hand-over (`live_until_ledger == 0`) keeps the
/// current admin, so it stays instant.
fn transfer_admin_role(e: &Env, new_admin: Address, live_until_ledger: u32) {
if live_until_ledger != 0 {
governance::require_setup_open(e);
}
storage::bump_instance(e);
access_control::transfer_admin_role(e, &new_admin, live_until_ledger);
}

/// Refused. A role admin could grant that role with no delay.
fn set_role_admin(e: &Env, _role: Symbol, _admin_role: Symbol) {
panic_with_error!(e, ShareTokenError::UseTimelock);
}

/// Refused. A token without an admin could never be unpaused, never rotate
/// the manager role and never replace the vault that holds it.
/// `transfer_admin_role` and `accept_admin_transfer` are the way out.
/// the manager role and never replace the vault that holds it. The way out
/// is a hand-over: propose and apply an `Admin` change (`transfer_admin_role`
/// during setup), then the nominee calls `accept_admin_transfer`.
fn renounce_admin(e: &Env) {
panic_with_error!(e, ShareTokenError::AdminRequired);
}
Expand Down Expand Up @@ -140,13 +235,42 @@ impl RWAToken for ShareToken {
#[only_admin]
fn set_compliance(e: &Env, compliance: Address, operator: Address) {
operator.require_auth();
governance::require_setup_open(e);
storage::bump_instance(e);
RWA::set_compliance(e, &compliance);
}
#[only_admin]
fn set_identity_verifier(e: &Env, identity_verifier: Address, operator: Address) {
operator.require_auth();
governance::require_setup_open(e);
storage::bump_instance(e);
RWA::set_identity_verifier(e, &identity_verifier);
}
}

/// Stands in for a later token version in the upgrade tests. Never deployed.
#[cfg(feature = "upgrade_probe")]
#[soroban_sdk::contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum ProbeError {
AlreadyMigrated = 7100,
}

#[cfg(feature = "upgrade_probe")]
#[contractimpl]
impl ShareToken {
#[only_admin]
pub fn migrate(e: &Env, caller: Address) {
caller.require_auth();
storage::bump_instance(e);
if upgradeable::get_schema_version(e) >= 2 {
panic_with_error!(e, ProbeError::AlreadyMigrated);
}
upgradeable::set_schema_version(e, 2);
}

pub fn probe(_e: &Env) -> bool {
true
}
}
33 changes: 31 additions & 2 deletions contracts/share-token/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,36 @@ use soroban_sdk::contracterror;
#[repr(u32)]
pub enum ShareTokenError {
/// The token must always have an admin: `pause`, `unpause` and role
/// management need it. The admin hands over through `transfer_admin_role`
/// and `accept_admin_transfer` instead.
/// management need it. Hand it over instead: propose and apply an `Admin`
/// change (`transfer_admin_role` during setup), then the nominee calls
/// `accept_admin_transfer`.
AdminRequired = 7000,
/// No pending change has this id.
ChangeNotFound = 7001,
/// The change's delay has not elapsed.
ChangeNotReady = 7002,
/// As many changes as the timelock holds already wait.
TooManyPendingChanges = 7003,
/// Changes are instant until setup finishes; there is nothing to propose yet.
SetupOpen = 7004,
/// A direct call cannot make this change. After setup it goes through
/// `propose_change`; setting a role admin is never allowed.
UseTimelock = 7005,
/// Only possible while setup is open, and only once.
SetupClosed = 7006,
/// Setup cannot finish before the token is bound to its vault.
VaultNotBound = 7007,
/// The vault is winding down, so no change may be proposed or applied.
WindDownActive = 7008,
/// The vault is paused, which holds every timelocked change.
VaultPaused = 7009,
/// The change's delay is shorter than the vault's notice, so an investor
/// could not complete an exit before it lands.
DelayBelowNotice = 7010,
/// An upgrade already waits. Cancel it before proposing another.
UpgradeAlreadyPending = 7011,
/// An admin hand-over offered during setup is still open. Setup finishes
/// once it is accepted, cancelled or expired, so no nominee from setup can
/// accept after changes start to wait.
AdminTransferPending = 7016,
}
33 changes: 33 additions & 0 deletions contracts/share-token/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use soroban_sdk::{contractevent, Address};

use crate::governance::TokenChange;

#[contractevent]
pub struct ChangeProposed {
#[topic]
pub id: u32,
pub change: TokenChange,
pub eta: u64,
}

#[contractevent]
pub struct ChangeCancelled {
#[topic]
pub id: u32,
}

#[contractevent]
pub struct ChangeApplied {
#[topic]
pub id: u32,
pub change: TokenChange,
}

#[contractevent]
pub struct SetupFinished {}

#[contractevent]
pub struct VaultBound {
#[topic]
pub vault: Address,
}
Loading
Loading