Skip to content

Commit 12e6549

Browse files
committed
standards: add proxy and timelock primitives
1 parent b3eff11 commit 12e6549

15 files changed

Lines changed: 1664 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub const INVALID_OWNER: &str = "DuskStandards: invalid owner";
1313
pub const ZERO_PRINCIPAL: &str = "DuskStandards: zero principal";
1414
pub const REPLAY: &str = "DuskStandards: replay";
1515
pub const INVALID_NONCE: &str = "DuskStandards: invalid nonce";
16+
pub const DELAY_NOT_ELAPSED: &str = "DuskStandards: delay not elapsed";
17+
pub const OPERATION_DONE: &str = "DuskStandards: operation already done";
1618
pub const OPERATION_UNKNOWN: &str = "DuskStandards: operation unknown";
1719
pub const INVALID_OPERATION: &str = "DuskStandards: invalid operation";
1820
pub const EXPIRED: &str = "DuskStandards: expired";
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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, RoleAuthorization};
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_with_roles(
63+
admin,
64+
[
65+
TIMELOCK_ADMIN_ROLE,
66+
PROPOSER_ROLE,
67+
EXECUTOR_ROLE,
68+
CANCELLER_ROLE,
69+
],
70+
);
71+
Self {
72+
access,
73+
timelock: Timelock::new(min_delay),
74+
self_principal,
75+
}
76+
}
77+
78+
/// Returns the controller principal used for self-governed operations.
79+
pub const fn self_principal(&self) -> Principal {
80+
self.self_principal
81+
}
82+
83+
/// Returns access-control state.
84+
pub const fn access(&self) -> &AccessControl {
85+
&self.access
86+
}
87+
88+
/// Returns timelock state.
89+
pub const fn timelock(&self) -> &Timelock {
90+
&self.timelock
91+
}
92+
93+
/// Returns a scheduled operation.
94+
pub fn get(&self, id: OperationId) -> Option<&ScheduledOperation> {
95+
self.timelock.get(id)
96+
}
97+
98+
/// Returns whether an account has a role.
99+
pub fn has_role(&self, role: Role, account: Principal) -> bool {
100+
self.access.has_role(role, account)
101+
}
102+
103+
/// Grants a role through the underlying access-control policy.
104+
pub fn grant_role(
105+
&mut self,
106+
authorization: RoleAuthorization,
107+
role: Role,
108+
account: Principal,
109+
) {
110+
self.access.grant_role(authorization, role, account);
111+
}
112+
113+
/// Revokes a role through the underlying access-control policy.
114+
pub fn revoke_role(
115+
&mut self,
116+
authorization: RoleAuthorization,
117+
role: Role,
118+
account: Principal,
119+
) {
120+
self.access.revoke_role(authorization, role, account);
121+
}
122+
123+
/// Updates the minimum delay. Caller must be the controller itself.
124+
///
125+
/// Composing contracts should reach this path through a scheduled
126+
/// operation, not through an arbitrary administrator call.
127+
pub fn set_min_delay(&mut self, caller: Principal, min_delay: u64) {
128+
if caller != self.self_principal {
129+
panic!("{}", error::UNAUTHORIZED);
130+
}
131+
self.timelock.set_min_delay(min_delay);
132+
}
133+
134+
/// Schedules a self-governed minimum-delay update.
135+
pub fn schedule_min_delay_change(
136+
&mut self,
137+
caller: Principal,
138+
id: OperationId,
139+
now: u64,
140+
min_delay: u64,
141+
) -> u64 {
142+
self.access.assert_role(PROPOSER_ROLE, caller);
143+
self.timelock
144+
.schedule(id, now, min_delay.to_be_bytes().to_vec())
145+
}
146+
147+
/// Executes a ready minimum-delay update as the controller itself.
148+
pub fn execute_min_delay_change(
149+
&mut self,
150+
caller: Principal,
151+
id: OperationId,
152+
now: u64,
153+
) -> u64 {
154+
self.access.assert_role(EXECUTOR_ROLE, caller);
155+
let op = self
156+
.timelock
157+
.get(id)
158+
.unwrap_or_else(|| panic!("{}", error::OPERATION_UNKNOWN));
159+
if op.done {
160+
panic!("{}", error::OPERATION_DONE);
161+
}
162+
if now < op.ready_at {
163+
panic!("{}", error::DELAY_NOT_ELAPSED);
164+
}
165+
let bytes: [u8; 8] = op
166+
.payload
167+
.as_slice()
168+
.try_into()
169+
.unwrap_or_else(|_| panic!("{}", error::INVALID_OPERATION));
170+
let min_delay = u64::from_be_bytes(bytes);
171+
self.timelock.execute(id, now);
172+
self.set_min_delay(self.self_principal, min_delay);
173+
min_delay
174+
}
175+
176+
/// Schedules an operation. Caller must have proposer role.
177+
pub fn schedule(
178+
&mut self,
179+
caller: Principal,
180+
id: OperationId,
181+
now: u64,
182+
payload: Vec<u8>,
183+
) -> u64 {
184+
self.access.assert_role(PROPOSER_ROLE, caller);
185+
self.timelock.schedule(id, now, payload)
186+
}
187+
188+
/// Cancels a pending operation. Caller must have canceller role.
189+
pub fn cancel(&mut self, caller: Principal, id: OperationId) {
190+
self.access.assert_role(CANCELLER_ROLE, caller);
191+
self.timelock.cancel(id);
192+
}
193+
194+
/// Executes a ready operation. Caller must have executor role.
195+
pub fn execute(
196+
&mut self,
197+
caller: Principal,
198+
id: OperationId,
199+
now: u64,
200+
) -> Vec<u8> {
201+
self.access.assert_role(EXECUTOR_ROLE, caller);
202+
self.timelock.execute(id, now)
203+
}
204+
}

0 commit comments

Comments
 (0)