Skip to content

Commit 3dafce0

Browse files
committed
standards: add proxy and timelock primitives
1 parent a096d57 commit 3dafce0

14 files changed

Lines changed: 1328 additions & 23 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
STANDARDS_EXAMPLES := standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/multisig_controller
1+
STANDARDS_EXAMPLES := standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/multisig_controller standards/examples/proxy_counter
22
STANDARDS_WASM_CONTRACTS := $(STANDARDS_EXAMPLES)
33
LEGACY_SUBDIRS := tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn
44
STANDARDS_PROPTEST_CASES ?= 8192

standards/Cargo.lock

Lines changed: 44 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

standards/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ members = [
44
"examples/drc20_roles_pausable",
55
"examples/drc721_collection",
66
"examples/multisig_controller",
7+
"examples/proxy_counter",
78
]
89

910
resolver = "2"

standards/dusk-contract-standards/src/core/error.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub const ZERO_PRINCIPAL: &str = "DuskStandards: zero principal";
1414
pub const REPLAY: &str = "DuskStandards: replay";
1515
pub const INVALID_NONCE: &str = "DuskStandards: invalid nonce";
1616
pub const DELAY_NOT_ELAPSED: &str = "DuskStandards: delay not elapsed";
17+
pub const OPERATION_DONE: &str = "DuskStandards: operation already done";
1718
pub const OPERATION_UNKNOWN: &str = "DuskStandards: operation unknown";
1819
pub const INVALID_OPERATION: &str = "DuskStandards: invalid operation";
1920
pub const EXPIRED: &str = "DuskStandards: expired";
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
//
5+
// Copyright (c) DUSK NETWORK. All rights reserved.
6+
7+
//! Role-gated timelock controller.
8+
9+
use alloc::vec::Vec;
10+
11+
use crate::access::{AccessControl, Role};
12+
use crate::core::{error, Principal};
13+
use crate::governance::{OperationId, ScheduledOperation, Timelock};
14+
15+
/// Role allowed to update timelock policy.
16+
pub const TIMELOCK_ADMIN_ROLE: Role = {
17+
let mut role = [0u8; 32];
18+
role[0] = 1;
19+
role
20+
};
21+
22+
/// Role allowed to schedule operations.
23+
pub const PROPOSER_ROLE: Role = {
24+
let mut role = [0u8; 32];
25+
role[0] = 2;
26+
role
27+
};
28+
29+
/// Role allowed to execute ready operations.
30+
pub const EXECUTOR_ROLE: Role = {
31+
let mut role = [0u8; 32];
32+
role[0] = 3;
33+
role
34+
};
35+
36+
/// Role allowed to cancel pending operations.
37+
pub const CANCELLER_ROLE: Role = {
38+
let mut role = [0u8; 32];
39+
role[0] = 4;
40+
role
41+
};
42+
43+
/// Timelock plus role-based caller policy.
44+
#[derive(Clone, Debug)]
45+
pub struct TimelockController {
46+
access: AccessControl,
47+
timelock: Timelock,
48+
self_principal: Principal,
49+
}
50+
51+
impl TimelockController {
52+
/// Creates a controller and grants all controller roles to `admin`.
53+
pub fn new(
54+
self_principal: Principal,
55+
admin: Principal,
56+
min_delay: u64,
57+
) -> Self {
58+
if self_principal.is_zero() || admin.is_zero() {
59+
panic!("{}", error::ZERO_PRINCIPAL);
60+
}
61+
let mut access = AccessControl::new();
62+
access.init_admin(admin);
63+
access.grant_role(admin, TIMELOCK_ADMIN_ROLE, admin);
64+
access.grant_role(admin, PROPOSER_ROLE, admin);
65+
access.grant_role(admin, EXECUTOR_ROLE, admin);
66+
access.grant_role(admin, CANCELLER_ROLE, admin);
67+
Self {
68+
access,
69+
timelock: Timelock::new(min_delay),
70+
self_principal,
71+
}
72+
}
73+
74+
/// Returns the controller principal used for self-governed operations.
75+
pub const fn self_principal(&self) -> Principal {
76+
self.self_principal
77+
}
78+
79+
/// Returns access-control state.
80+
pub const fn access(&self) -> &AccessControl {
81+
&self.access
82+
}
83+
84+
/// Returns timelock state.
85+
pub const fn timelock(&self) -> &Timelock {
86+
&self.timelock
87+
}
88+
89+
/// Returns a scheduled operation.
90+
pub fn get(&self, id: OperationId) -> Option<&ScheduledOperation> {
91+
self.timelock.get(id)
92+
}
93+
94+
/// Returns whether an account has a role.
95+
pub fn has_role(&self, role: Role, account: Principal) -> bool {
96+
self.access.has_role(role, account)
97+
}
98+
99+
/// Grants a role through the underlying access-control policy.
100+
pub fn grant_role(
101+
&mut self,
102+
caller: Principal,
103+
role: Role,
104+
account: Principal,
105+
) {
106+
self.access.grant_role(caller, role, account);
107+
}
108+
109+
/// Revokes a role through the underlying access-control policy.
110+
pub fn revoke_role(
111+
&mut self,
112+
caller: Principal,
113+
role: Role,
114+
account: Principal,
115+
) {
116+
self.access.revoke_role(caller, role, account);
117+
}
118+
119+
/// Updates the minimum delay. Caller must be the controller itself.
120+
///
121+
/// Composing contracts should reach this path through a scheduled
122+
/// operation, not through an arbitrary administrator call.
123+
pub fn set_min_delay(&mut self, caller: Principal, min_delay: u64) {
124+
if caller != self.self_principal {
125+
panic!("{}", error::UNAUTHORIZED);
126+
}
127+
self.timelock.set_min_delay(min_delay);
128+
}
129+
130+
/// Schedules a self-governed minimum-delay update.
131+
pub fn schedule_min_delay_change(
132+
&mut self,
133+
caller: Principal,
134+
id: OperationId,
135+
now: u64,
136+
min_delay: u64,
137+
) -> u64 {
138+
self.access.assert_role(PROPOSER_ROLE, caller);
139+
self.timelock
140+
.schedule(id, now, min_delay.to_be_bytes().to_vec())
141+
}
142+
143+
/// Executes a ready minimum-delay update as the controller itself.
144+
pub fn execute_min_delay_change(
145+
&mut self,
146+
caller: Principal,
147+
id: OperationId,
148+
now: u64,
149+
) -> u64 {
150+
self.access.assert_role(EXECUTOR_ROLE, caller);
151+
let op = self
152+
.timelock
153+
.get(id)
154+
.unwrap_or_else(|| panic!("{}", error::OPERATION_UNKNOWN));
155+
if op.done {
156+
panic!("{}", error::OPERATION_DONE);
157+
}
158+
if now < op.ready_at {
159+
panic!("{}", error::DELAY_NOT_ELAPSED);
160+
}
161+
let bytes: [u8; 8] = op
162+
.payload
163+
.as_slice()
164+
.try_into()
165+
.unwrap_or_else(|_| panic!("{}", error::INVALID_OPERATION));
166+
let min_delay = u64::from_be_bytes(bytes);
167+
self.timelock.execute(id, now);
168+
self.set_min_delay(self.self_principal, min_delay);
169+
min_delay
170+
}
171+
172+
/// Schedules an operation. Caller must have proposer role.
173+
pub fn schedule(
174+
&mut self,
175+
caller: Principal,
176+
id: OperationId,
177+
now: u64,
178+
payload: Vec<u8>,
179+
) -> u64 {
180+
self.access.assert_role(PROPOSER_ROLE, caller);
181+
self.timelock.schedule(id, now, payload)
182+
}
183+
184+
/// Cancels a pending operation. Caller must have canceller role.
185+
pub fn cancel(&mut self, caller: Principal, id: OperationId) {
186+
self.access.assert_role(CANCELLER_ROLE, caller);
187+
self.timelock.cancel(id);
188+
}
189+
190+
/// Executes a ready operation. Caller must have executor role.
191+
pub fn execute(
192+
&mut self,
193+
caller: Principal,
194+
id: OperationId,
195+
now: u64,
196+
) -> Vec<u8> {
197+
self.access.assert_role(EXECUTOR_ROLE, caller);
198+
self.timelock.execute(id, now)
199+
}
200+
}

0 commit comments

Comments
 (0)