forked from Lilly-Protocol/lily-contracts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlib.rs
More file actions
220 lines (184 loc) · 7.29 KB
/
Copy pathlib.rs
File metadata and controls
220 lines (184 loc) · 7.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#![no_std]
//! Agent wallet binding and policy contract.
use lily_common::{bump_instance, require, ProtocolError};
use soroban_sdk::{
contract, contractimpl, contracttype, symbol_short, unwrap::UnwrapOptimized, Address, Env,
Symbol,
};
#[contract]
pub struct WalletContract;
/// Wallet contract schema version.
pub const SCHEMA_VERSION: u32 = 1;
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WalletBinding {
pub wallet: Address,
pub settlement_asset: Symbol,
pub spend_limit: i128,
pub enabled: bool,
pub revision: u64,
}
/// Storage keys for wallet policy configuration and agent binding records.
#[contracttype]
#[derive(Clone)]
enum DataKey {
/// Stores the wallet policy registry admin `Address`. Durability: Instance.
Admin,
/// Marker boolean indicating if the contract has been initialized. Durability: Instance.
Initialized,
/// Stores the schema version (`u32`). Durability: Instance.
SchemaVersion,
/// Maps an agent `Address` to their `WalletBinding` configuration. Durability: Persistent.
Binding(Address),
PinnedAdmin,
}
#[contractimpl]
impl WalletContract {
/// Capture the intended initial admin at deploy time.
///
/// `initialize` only accepts this exact address, so a front-runner cannot
/// claim a fresh deployment with their own admin.
pub fn __constructor(env: Env, initial_admin: Address) {
env.storage().instance().set(&DataKey::PinnedAdmin, &initial_admin);
}
/// Initialize the wallet policy registry.
///
/// The initial admin must match the address pinned by the constructor at
/// deploy time, preventing initialization front-running.
pub fn initialize(env: Env, admin: Address) {
admin.require_auth();
require(
&env,
!env.storage().instance().has(&DataKey::Initialized),
ProtocolError::AlreadyInitialized,
);
require_auth_or_error(&admin, &env);
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::SchemaVersion, &SCHEMA_VERSION);
env.storage().instance().set(&DataKey::Initialized, &true);
bump_instance(&env);
env.events().publish((symbol_short!("init"),), admin);
}
/// Return whether the contract has been initialized.
pub fn is_initialized(env: Env) -> bool {
env.storage().instance().has(&DataKey::Initialized)
}
/// Bind an agent to a settlement wallet and policy envelope.
///
/// Fails if the agent already has any binding (enabled or disabled).
/// Use `rebind_wallet` to explicitly replace an existing binding.
pub fn bind_wallet(
env: Env,
agent: Address,
wallet: Address,
settlement_asset: Symbol,
spend_limit: i128,
) {
ensure_initialized(&env);
require(&env, spend_limit > 0, ProtocolError::InvalidInput);
require_auth_or_error(&agent, &env);
require_auth_or_error(&wallet, &env);
let key = DataKey::Binding(agent.clone());
require(&env, !env.storage().persistent().has(&key), ProtocolError::WalletAlreadyBound);
let binding =
WalletBinding { wallet, settlement_asset, spend_limit, enabled: true, revision: 0 };
env.storage().persistent().set(&key, &binding);
bump_instance(&env);
env.events().publish((symbol_short!("bind"), agent), binding);
}
/// Explicitly replace an existing wallet binding.
///
/// Requires the agent to already have a binding. The new binding starts at
/// revision 0 and is enabled. This removes the silent overwrite behavior
/// that `bind_wallet` previously performed on disabled bindings.
pub fn rebind_wallet(
env: Env,
agent: Address,
wallet: Address,
settlement_asset: Symbol,
spend_limit: i128,
) {
ensure_initialized(&env);
require(&env, spend_limit > 0, ProtocolError::InvalidInput);
agent.require_auth();
wallet.require_auth();
let key = DataKey::Binding(agent.clone());
require(&env, env.storage().persistent().has(&key), ProtocolError::MissingRecord);
let binding = WalletBinding {
wallet,
settlement_asset,
spend_limit,
enabled: true,
revision: next_revision,
};
env.storage().persistent().set(&key, &binding);
bump_instance(&env);
env.events().publish((symbol_short!("rebind"), agent), binding);
}
/// Update the spend limit for an enabled binding.
pub fn update_spend_limit(env: Env, agent: Address, spend_limit: i128) {
ensure_initialized(&env);
require(&env, spend_limit > 0, ProtocolError::InvalidInput);
require_auth_or_error(&agent, &env);
let mut binding = get_binding_internal(&env, &agent);
require_enabled(&env, binding.enabled);
binding.spend_limit = spend_limit;
binding.revision = checked_inc(&env, binding.revision);
env.storage().persistent().set(&DataKey::Binding(agent.clone()), &binding);
bump_instance(&env);
env.events().publish((symbol_short!("limit"), agent), binding);
}
/// Enable or disable a wallet binding.
pub fn set_enabled(env: Env, agent: Address, enabled: bool) {
ensure_initialized(&env);
require_auth_or_error(&agent, &env);
let mut binding = get_binding_internal(&env, &agent);
binding.enabled = enabled;
binding.revision = checked_inc(&env, binding.revision);
env.storage().persistent().set(&DataKey::Binding(agent.clone()), &binding);
bump_instance(&env);
env.events().publish((symbol_short!("state"), agent), binding);
}
/// Admin emergency deactivation of a wallet binding.
pub fn admin_deactivate(env: Env, agent: Address) {
ensure_initialized(&env);
let admin = get_admin(&env);
admin.require_auth();
let mut binding = get_binding_internal(&env, &agent);
binding.enabled = false;
binding.revision += 1;
env.storage().persistent().set(&DataKey::Binding(agent.clone()), &binding);
bump_instance(&env);
env.events().publish((symbol_short!("adm_deact"), agent), binding);
}
/// Read the current binding for an agent.
#[must_use]
pub fn get_binding(env: Env, agent: Address) -> WalletBinding {
ensure_initialized(&env);
bump_instance(&env);
get_binding_internal(&env, &agent)
}
/// Read the current binding for an agent if one exists, returning `None` otherwise.
pub fn get_binding_opt(env: Env, agent: Address) -> Option<WalletBinding> {
ensure_initialized(&env);
bump_instance(&env);
env.storage().persistent().get(&DataKey::Binding(agent))
}
}
fn ensure_initialized(env: &Env) {
require(
env,
env.storage().instance().has(&DataKey::Initialized),
ProtocolError::NotInitialized,
);
}
fn get_admin(env: &Env) -> Address {
env.storage().instance().get(&DataKey::Admin).unwrap_optimized()
}
fn get_binding_internal(env: &Env, agent: &Address) -> WalletBinding {
env.storage()
.persistent()
.get(&DataKey::Binding(agent.clone()))
.unwrap_or_else(|| soroban_sdk::panic_with_error!(env, ProtocolError::MissingRecord))
}
mod test;