Skip to content
Merged
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.

4 changes: 4 additions & 0 deletions contracts/compliance/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ soroban-sdk = { workspace = true }
stellar-access = { workspace = true }
stellar-macros = { workspace = true }
stellar-tokens = { workspace = true }
storage = { path = "../../crates/storage" }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
35 changes: 35 additions & 0 deletions contracts/compliance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,66 @@ pub struct Compliance;
impl Compliance {
pub fn __constructor(e: &Env, admin: Address) {
access_control::set_admin(e, &admin);
storage::bump_instance(e);
}
}

#[contractimpl(contracttrait)]
impl token_binder::TokenBinder for Compliance {
#[only_admin]
fn bind_token(e: &Env, token: Address, _operator: Address) {
storage::bump_instance(e);
token_binder::bind_token(e, &token);
}

#[only_admin]
fn unbind_token(e: &Env, token: Address, _operator: Address) {
storage::bump_instance(e);
token_binder::unbind_token(e, &token);
}
}

/// The token calls the check before and the hook after every mint, burn and
/// transfer, which is what keeps this instance alive.
#[contractimpl(contracttrait)]
impl compliance::Compliance for Compliance {
#[only_admin]
fn add_module_to(e: &Env, hook: ComplianceHook, module: Address, _operator: Address) {
storage::bump_instance(e);
compliance::storage::add_module_to(e, hook, module);
}

#[only_admin]
fn remove_module_from(e: &Env, hook: ComplianceHook, module: Address, _operator: Address) {
storage::bump_instance(e);
compliance::storage::remove_module_from(e, hook, module);
}

fn transferred(e: &Env, from: Address, to: Address, amount: i128, token: Address) {
storage::bump_instance(e);
compliance::storage::transferred(e, from, to, amount, token);
}

fn created(e: &Env, to: Address, amount: i128, token: Address) {
storage::bump_instance(e);
compliance::storage::created(e, to, amount, token);
}

fn destroyed(e: &Env, from: Address, amount: i128, token: Address) {
storage::bump_instance(e);
compliance::storage::destroyed(e, from, amount, token);
}

fn can_transfer(e: &Env, from: Address, to: Address, amount: i128, token: Address) -> bool {
storage::bump_instance(e);
compliance::storage::can_transfer(e, from, to, amount, token)
}

fn can_create(e: &Env, to: Address, amount: i128, token: Address) -> bool {
storage::bump_instance(e);
compliance::storage::can_create(e, to, amount, token)
}
}

#[cfg(test)]
mod test;
73 changes: 73 additions & 0 deletions contracts/compliance/src/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use soroban_sdk::{
testutils::{storage::Instance as _, Address as _, Ledger as _},
Address, Env,
};
use storage::INSTANCE_EXTEND_AMOUNT;

use crate::{Compliance, ComplianceClient};

const TWO_DAYS_IN_LEDGERS: u32 = 2 * 17_280;

fn instance_ttl(e: &Env, id: &Address) -> u32 {
e.as_contract(id, || e.storage().instance().get_ttl())
}

fn age(e: &Env) {
e.ledger()
.with_mut(|l| l.sequence_number += TWO_DAYS_IN_LEDGERS);
}

/// A compliance contract with one bound token, its instance decayed past the
/// extend threshold.
fn decayed(e: &Env) -> (ComplianceClient<'_>, Address) {
e.mock_all_auths();
let admin = Address::generate(e);
let token = Address::generate(e);
let client = ComplianceClient::new(e, &e.register(Compliance, (admin.clone(),)));
client.bind_token(&token, &admin);
age(e);
assert!(instance_ttl(e, &client.address) < INSTANCE_EXTEND_AMOUNT);
(client, token)
}

#[test]
fn construction_writes_the_instance_with_the_full_ttl() {
let e = Env::default();
let id = e.register(Compliance, (Address::generate(&e),));

assert_eq!(instance_ttl(&e, &id), INSTANCE_EXTEND_AMOUNT);
}

#[test]
fn the_checks_a_token_runs_before_moving_shares_restore_the_instance_ttl() {
let e = Env::default();
let (client, token) = decayed(&e);
let holder = Address::generate(&e);
let other = Address::generate(&e);

assert!(client.can_create(&holder, &1, &token));
assert_eq!(instance_ttl(&e, &client.address), INSTANCE_EXTEND_AMOUNT);

age(&e);
assert!(client.can_transfer(&holder, &other, &1, &token));
assert_eq!(instance_ttl(&e, &client.address), INSTANCE_EXTEND_AMOUNT);
}

#[test]
fn the_hooks_a_token_calls_after_moving_shares_restore_the_instance_ttl() {
let e = Env::default();
let (client, token) = decayed(&e);
let holder = Address::generate(&e);
let other = Address::generate(&e);

client.created(&holder, &1, &token);
assert_eq!(instance_ttl(&e, &client.address), INSTANCE_EXTEND_AMOUNT);

age(&e);
client.transferred(&holder, &other, &1, &token);
assert_eq!(instance_ttl(&e, &client.address), INSTANCE_EXTEND_AMOUNT);

age(&e);
client.destroyed(&other, &1, &token);
assert_eq!(instance_ttl(&e, &client.address), INSTANCE_EXTEND_AMOUNT);
}
6 changes: 6 additions & 0 deletions contracts/identity-verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,29 @@ pub struct IdentityVerifier;
impl IdentityVerifier {
pub fn __constructor(e: &Env, admin: Address) {
access_control::set_admin(e, &admin);
storage::bump_instance(e);
}

/// Adds or removes an account from the allowlist.
#[only_admin]
pub fn allow(e: &Env, account: Address, allowed: bool, caller: Address) {
caller.require_auth();
storage::bump_instance(e);
storage::set_persistent(e, &DataKey::Allowed(account), &allowed);
}

pub fn is_allowed(e: &Env, account: Address) -> bool {
storage::bump_instance(e);
storage::get_persistent(e, &DataKey::Allowed(account)).unwrap_or(false)
}
}

#[contractimpl(contracttrait)]
impl identity_verification::IdentityVerifier for IdentityVerifier {
/// The share token calls this on every mint and transfer, so it is what
/// keeps the instance alive.
fn verify_identity(e: &Env, account: &Address) {
storage::bump_instance(e);
let allowed: bool =
storage::get_persistent(e, &DataKey::Allowed(account.clone())).unwrap_or(false);
if !allowed {
Expand Down
39 changes: 37 additions & 2 deletions contracts/identity-verifier/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use super::*;
use soroban_sdk::{
testutils::{storage::Persistent as _, Address as _, Ledger as _},
testutils::{storage::Instance as _, storage::Persistent as _, Address as _, Ledger as _},
Address, Env,
};
use storage::PERSISTENT_EXTEND_AMOUNT;
use storage::{INSTANCE_EXTEND_AMOUNT, PERSISTENT_EXTEND_AMOUNT};

const DAY_IN_LEDGERS: u32 = 17_280;
use stellar_tokens::rwa::identity_verification::IdentityVerifierClient as IdClient;
Expand Down Expand Up @@ -103,3 +103,38 @@ fn checking_an_allowed_account_restores_the_full_ttl() {
IdClient::new(&e, &id).verify_identity(&investor);
assert_eq!(allowed_ttl(&e, &id, &investor), PERSISTENT_EXTEND_AMOUNT);
}

fn instance_ttl(e: &Env, id: &Address) -> u32 {
e.as_contract(id, || e.storage().instance().get_ttl())
}

#[test]
fn construction_writes_the_instance_with_the_full_ttl() {
let e = Env::default();
let id = e.register(IdentityVerifier, (Address::generate(&e),));

assert_eq!(instance_ttl(&e, &id), INSTANCE_EXTEND_AMOUNT);
}

#[test]
fn allowing_and_checking_restore_the_instance_ttl() {
let e = Env::default();
e.mock_all_auths();
let admin = Address::generate(&e);
let investor = Address::generate(&e);
let id = e.register(IdentityVerifier, (admin.clone(),));
let client = IdentityVerifierClient::new(&e, &id);

age(&e, 2 * DAY_IN_LEDGERS);
assert!(instance_ttl(&e, &id) < INSTANCE_EXTEND_AMOUNT);
client.allow(&investor, &true, &admin);
assert_eq!(instance_ttl(&e, &id), INSTANCE_EXTEND_AMOUNT);

age(&e, 2 * DAY_IN_LEDGERS);
IdClient::new(&e, &id).verify_identity(&investor);
assert_eq!(instance_ttl(&e, &id), INSTANCE_EXTEND_AMOUNT);

age(&e, 2 * DAY_IN_LEDGERS);
client.is_allowed(&investor);
assert_eq!(instance_ttl(&e, &id), INSTANCE_EXTEND_AMOUNT);
}
1 change: 1 addition & 0 deletions contracts/share-token/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ stellar-access = { workspace = true }
stellar-macros = { workspace = true }
stellar-tokens = { workspace = true }
stellar-contract-utils = { workspace = true }
storage = { path = "../../crates/storage" }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
Expand Down
37 changes: 36 additions & 1 deletion contracts/share-token/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use stellar_access::access_control::{self as access_control, AccessControl};
use stellar_contract_utils::pausable::{self as pausable, Pausable};
use stellar_macros::{only_admin, only_any_role, only_role};
use stellar_tokens::{
fungible::{Base, FungibleToken},
fungible::{Base, ContractOverrides, FungibleToken},
rwa::{RWAToken, RWA},
};

Expand Down Expand Up @@ -36,6 +36,7 @@ impl ShareToken {
);
RWA::set_compliance(e, &compliance);
RWA::set_identity_verifier(e, &identity_verifier);
storage::bump_instance(e);
}
}

Expand All @@ -44,18 +45,43 @@ impl Pausable for ShareToken {
#[only_admin]
fn pause(e: &Env, caller: Address) {
caller.require_auth();
storage::bump_instance(e);
pausable::pause(e);
}
#[only_admin]
fn unpause(e: &Env, caller: Address) {
caller.require_auth();
storage::bump_instance(e);
pausable::unpause(e);
}
}

/// OpenZeppelin leaves the instance's lifetime to the token, so every call
/// that moves shares extends it before handing over to the library.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also extend in balance and total_supply? A later wind-down claim only reads balance, so a quiet wind-down could still let the instance lapse.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in #276 (9516c95): balance and total_supply now extend the token's instance before they read, so a wind-down that only reads balances keeps the token alive. A test lets the instance fall below its threshold and checks that a balance read brings it back to the full 30 days.

#[contractimpl(contracttrait)]
impl FungibleToken for ShareToken {
type ContractType = RWA;

fn transfer(e: &Env, from: Address, to: MuxedAddress, amount: i128) {
storage::bump_instance(e);
<Self::ContractType as ContractOverrides>::transfer(e, &from, &to, amount);
}

fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) {
storage::bump_instance(e);
<Self::ContractType as ContractOverrides>::transfer_from(e, &spender, &from, &to, amount);
}

fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) {
storage::bump_instance(e);
<Self::ContractType as ContractOverrides>::approve(
e,
&owner,
&spender,
amount,
live_until_ledger,
);
}
}

#[contractimpl(contracttrait)]
Expand All @@ -72,15 +98,18 @@ impl AccessControl for ShareToken {
impl RWAToken for ShareToken {
#[only_role(operator, "manager")]
fn mint(e: &Env, to: Address, amount: i128, operator: Address) {
storage::bump_instance(e);
RWA::mint(e, &to, amount);
}
#[only_role(operator, "manager")]
fn burn(e: &Env, user_address: Address, amount: i128, operator: Address) {
storage::bump_instance(e);
RWA::burn(e, &user_address, amount);
}
/// The vault escrows redemptions through it; compliance intervenes with it.
#[only_any_role(operator, ["manager", "compliance"])]
fn forced_transfer(e: &Env, from: Address, to: Address, amount: i128, operator: Address) {
storage::bump_instance(e);
RWA::forced_transfer(e, &from, &to, amount);
}
#[only_role(operator, "compliance")]
Expand All @@ -90,28 +119,34 @@ impl RWAToken for ShareToken {
new_account: Address,
operator: Address,
) -> bool {
storage::bump_instance(e);
RWA::recover_balance(e, &old_account, &new_account)
}
#[only_role(operator, "compliance")]
fn set_address_frozen(e: &Env, user_address: Address, freeze: bool, operator: Address) {
storage::bump_instance(e);
RWA::set_address_frozen(e, &user_address, freeze);
}
#[only_role(operator, "compliance")]
fn freeze_partial_tokens(e: &Env, user_address: Address, amount: i128, operator: Address) {
storage::bump_instance(e);
RWA::freeze_partial_tokens(e, &user_address, amount);
}
#[only_role(operator, "compliance")]
fn unfreeze_partial_tokens(e: &Env, user_address: Address, amount: i128, operator: Address) {
storage::bump_instance(e);
RWA::unfreeze_partial_tokens(e, &user_address, amount);
}
#[only_admin]
fn set_compliance(e: &Env, compliance: Address, operator: Address) {
operator.require_auth();
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();
storage::bump_instance(e);
RWA::set_identity_verifier(e, &identity_verifier);
}
}
Loading
Loading