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: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ authors = ["Moonlight"]
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/Moonlight-Protocol/soroban-core"
version = "0.2.1"
version = "0.3.0"


[workspace.dependencies]
Expand Down
45 changes: 45 additions & 0 deletions contracts/channel-auth/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ pub struct ProviderRemoved {
pub provider: Address,
}

// UC6: the council's quorum-authorized record that an asset channel was enabled or disabled.
// The contract holds NO channel/asset state — this event is the only on-chain artifact. The
// council-platform DB (sole authoritative writer) and every provider converge on it: `enabled`
// distinguishes enable/re-enable (true) from disable (false). `channel` is the privacy-channel
// contract id; `asset` is its token contract id (a channel is single-asset, so this is self-describing).
#[contractevent(data_format = "single-value")]
pub struct ChannelStateChanged {
#[topic]
pub channel: Address,
#[topic]
pub asset: Address,
pub enabled: bool,
}

// MOON-09: emit a dedicated event on upgrade so the governance audit trail does not rely solely on
// the raw Stellar transaction record.
#[contractevent(data_format = "single-value")]
Expand Down Expand Up @@ -110,6 +124,37 @@ impl ChannelAuthContract {
}
}

// UC6: asset-lifecycle. Quorum-gated, event-only — the contract stores no channel/asset state;
// it only emits the quorum-authorized record that the council DB and providers converge on. The
// owner is the council quorum account, so `enforce_owner_auth` is the quorum gate (mirrors
// add_provider/remove_provider).
#[contractimpl]
impl ChannelAuthContract {
/// Enable an asset `channel` for service. Also used to RE-ENABLE a previously disabled
/// channel — both resume full service, so both emit `ChannelStateChanged { enabled: true }`.
pub fn enable_channel(e: &Env, channel: Address, asset: Address) {
ownable::enforce_owner_auth(e);
ChannelStateChanged {
channel,
asset,
enabled: true,
}
.publish(e);
}

/// Disable an asset `channel`. The channel becomes withdraw-only (new deposits/sends rejected);
/// that enforcement lives provider-side. Emits `ChannelStateChanged { enabled: false }`.
pub fn disable_channel(e: &Env, channel: Address, asset: Address) {
ownable::enforce_owner_auth(e);
ChannelStateChanged {
channel,
asset,
enabled: false,
}
.publish(e);
}
}

#[contractimpl]
impl CustomAccountInterface for ChannelAuthContract {
type Error = MoonlightError;
Expand Down
160 changes: 159 additions & 1 deletion contracts/channel-auth/src/tests/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,47 @@ use soroban_sdk::{

use super::tests::create_contract;
use crate::contract::ChannelAuthContractClient;
use crate::contract::{ContractInitialized, ProviderAdded, ProviderRemoved};
use crate::contract::{ChannelStateChanged, ContractInitialized, ProviderAdded, ProviderRemoved};

fn enable_channel_with_auth(
client: &ChannelAuthContractClient,
admin: &Address,
channel: &Address,
asset: &Address,
e: &Env,
) {
client
.mock_auths(&[MockAuth {
address: admin,
invoke: &MockAuthInvoke {
contract: &client.address,
fn_name: "enable_channel",
args: (channel, asset).into_val(e),
sub_invokes: &[],
},
}])
.enable_channel(channel, asset);
}

fn disable_channel_with_auth(
client: &ChannelAuthContractClient,
admin: &Address,
channel: &Address,
asset: &Address,
e: &Env,
) {
client
.mock_auths(&[MockAuth {
address: admin,
invoke: &MockAuthInvoke {
contract: &client.address,
fn_name: "disable_channel",
args: (channel, asset).into_val(e),
sub_invokes: &[],
},
}])
.disable_channel(channel, asset);
}

fn add_provider_with_auth(
client: &ChannelAuthContractClient,
Expand Down Expand Up @@ -103,3 +143,121 @@ fn test_provider_lifecycle_with_events() {
remove_provider_with_auth(&client, &admin, &provider, &e);
assert!(!client.is_provider(&provider));
}

#[test]
fn test_enable_channel_emits_event() {
let e = Env::default();
let (client, admin) = create_contract(&e);
let channel = Address::generate(&e);
let asset = Address::generate(&e);

enable_channel_with_auth(&client, &admin, &channel, &asset, &e);

let events = e.events().all();
let last = events.events().last().unwrap();
assert_eq!(
last,
&ChannelStateChanged {
channel,
asset,
enabled: true,
}
.to_xdr(&e, &client.address)
);
}

#[test]
fn test_disable_channel_emits_event() {
let e = Env::default();
let (client, admin) = create_contract(&e);
let channel = Address::generate(&e);
let asset = Address::generate(&e);

disable_channel_with_auth(&client, &admin, &channel, &asset, &e);

let events = e.events().all();
let last = events.events().last().unwrap();
assert_eq!(
last,
&ChannelStateChanged {
channel,
asset,
enabled: false,
}
.to_xdr(&e, &client.address)
);
}

// Enable -> disable -> re-enable. Re-enable reuses enable_channel and emits the same
// `enabled: true` record, so providers and the council DB resume full service on it.
#[test]
fn test_channel_lifecycle_enable_disable_reenable() {
let e = Env::default();
let (client, admin) = create_contract(&e);
let channel = Address::generate(&e);
let asset = Address::generate(&e);

enable_channel_with_auth(&client, &admin, &channel, &asset, &e);
disable_channel_with_auth(&client, &admin, &channel, &asset, &e);
enable_channel_with_auth(&client, &admin, &channel, &asset, &e);

let events = e.events().all();
let last = events.events().last().unwrap();
assert_eq!(
last,
&ChannelStateChanged {
channel,
asset,
enabled: true,
}
.to_xdr(&e, &client.address)
);
}

// Quorum gate: a call NOT authorized by the owner (council quorum) must be rejected, and emit
// no event. Mirrors the add_provider non-owner rejection in tests.rs.
#[test]
fn test_enable_channel_requires_owner_auth() {
let e = Env::default();
let (client, _admin) = create_contract(&e);
let not_owner = Address::generate(&e);
let channel = Address::generate(&e);
let asset = Address::generate(&e);

let res = client
.mock_auths(&[MockAuth {
address: &not_owner,
invoke: &MockAuthInvoke {
contract: &client.address,
fn_name: "enable_channel",
args: (&channel, &asset).into_val(&e),
sub_invokes: &[],
},
}])
.try_enable_channel(&channel, &asset);

assert!(res.is_err());
}

#[test]
fn test_disable_channel_requires_owner_auth() {
let e = Env::default();
let (client, _admin) = create_contract(&e);
let not_owner = Address::generate(&e);
let channel = Address::generate(&e);
let asset = Address::generate(&e);

let res = client
.mock_auths(&[MockAuth {
address: &not_owner,
invoke: &MockAuthInvoke {
contract: &client.address,
fn_name: "disable_channel",
args: (&channel, &asset).into_val(&e),
sub_invokes: &[],
},
}])
.try_disable_channel(&channel, &asset);

assert!(res.is_err());
}
Loading